66b0a488c7e1a9690f055bd66fba4453eb7df82d
[civicrm-core.git] / tests / phpunit / CiviTest / CiviMailUtils.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * Mail utils for use during unit testing to allow retrieval
14 * and examination of 'sent' emails.
15 *
16 * Basic usage:
17 *
18 * $mut = new CiviMailUtils( $this, true ); //true automatically starts spooling
19 * ... do stuff ...
20 * $msg = $mut->getMostRecentEmail( 'raw' ); // or 'ezc' to get an ezc mail object
21 * ... assert stuff about $msg ...
22 * $mut->stop();
23 *
24 *
25 * @package CiviCRM
26 */
27
28 /**
29 * Class CiviMailUtils
30 */
31 class CiviMailUtils extends PHPUnit\Framework\TestCase {
32
33 /**
34 * Current outbound email option
35 * @var mixed
36 */
37 protected $_outBound_option = NULL;
38
39 /**
40 * Is this a webtest
41 * @var bool
42 */
43 protected $_webtest = FALSE;
44
45 /**
46 * Constructor.
47 *
48 * @param CiviSeleniumTestCase|CiviUnitTestCase $unit_test The currently running test
49 * @param bool $startImmediately
50 * Start writing to db now or wait until start() is called.
51 */
52 public function __construct(&$unit_test, $startImmediately = TRUE) {
53 $this->_ut = $unit_test;
54
55 // Check if running under webtests or not
56 if (is_subclass_of($unit_test, 'CiviSeleniumTestCase')) {
57 $this->_webtest = TRUE;
58 }
59
60 if ($startImmediately) {
61 $this->start();
62 }
63 }
64
65 /**
66 * Clean up after test.
67 *
68 * @throws \CRM_Core_Exception
69 */
70 public function __destruct() {
71 $this->stop();
72 $this->clearMessages();
73 }
74
75 /**
76 * Start writing emails to db instead of current option.
77 */
78 public function start() {
79 if ($this->_webtest) {
80 // Change outbound mail setting
81 $this->_ut->openCiviPage('admin/setting/smtp', "reset=1", "_qf_Smtp_next");
82
83 // First remember the current setting
84 $this->_outBound_option = $this->getSelectedOutboundOption();
85
86 $this->_ut->click('xpath=//input[@name="outBound_option" and @value="' . CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB . '"]');
87 $this->_ut->clickLink("_qf_Smtp_next");
88
89 // Is there supposed to be a status message displayed when outbound email settings are changed?
90 // assert something?
91
92 }
93 else {
94
95 // save current setting for outbound option, then change it
96 $mailingBackend = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME,
97 'mailing_backend'
98 );
99
100 $this->_outBound_option = $mailingBackend['outBound_option'];
101 $mailingBackend['outBound_option'] = CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB;
102
103 Civi::settings()->set('mailing_backend', $mailingBackend);
104
105 $mailingBackend = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME,
106 'mailing_backend'
107 );
108 }
109 }
110
111 public function stop() {
112 if ($this->_webtest) {
113 if ($this->_outBound_option != CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB) {
114 // Change outbound mail setting
115 $this->_ut->openCiviPage('admin/setting/smtp', "reset=1", "_qf_Smtp_next");
116 $this->_ut->click('xpath=//input[@name="outBound_option" and @value="' . $this->_outBound_option . '"]');
117 // There will be a warning when switching from test to live mode
118 if ($this->_outBound_option != CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED) {
119 $this->_ut->getAlert();
120 }
121 $this->_ut->clickLink("_qf_Smtp_next");
122 }
123 }
124 else {
125
126 $mailingBackend = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME,
127 'mailing_backend'
128 );
129
130 $mailingBackend['outBound_option'] = $this->_outBound_option;
131
132 Civi::settings()->set('mailing_backend', $mailingBackend);
133 }
134 }
135
136 /**
137 * @param string $type
138 *
139 * @return ezcMail|string
140 */
141 public function getMostRecentEmail($type = 'raw') {
142 $msg = '';
143
144 if ($this->_webtest) {
145 // I don't understand but for some reason we have to load the page twice for a recent mailing to appear.
146 $this->_ut->openCiviPage('mailing/browse/archived', 'reset=1');
147 $this->_ut->openCiviPage('mailing/browse/archived', 'reset=1', 'css=td.crm-mailing-name');
148 }
149 // We can't fetch mailing headers from webtest so we'll only try if the format is raw
150 if ($this->_webtest && $type == 'raw') {
151 // This should select the first "Report" link in the table, which is sorted by Completion Date descending, so in theory is the most recent email. Not sure of a more robust way at the moment.
152 $this->_ut->clickLink('xpath=//tr[contains(@id, "crm-mailing_")]//a[text()="Report"]');
153
154 // Also not sure how robust this is, but there isn't a good
155 // identifier for this link either.
156 $this->_ut->waitForElementPresent('xpath=//a[contains(text(), "View complete message")]');
157 $this->_ut->clickAjaxLink('xpath=//a[contains(text(), "View complete message")]');
158 $msg = $this->_ut->getText('css=.ui-dialog-content.crm-ajax-container');
159 }
160 else {
161 $dao = CRM_Core_DAO::executeQuery('SELECT headers, body FROM civicrm_mailing_spool ORDER BY id DESC LIMIT 1');
162 if ($dao->fetch()) {
163 $msg = $dao->headers . "\n\n" . $dao->body;
164 }
165 }
166
167 switch ($type) {
168 case 'raw':
169 // nothing to do
170 break;
171
172 case 'ezc':
173 $msg = $this->convertToEzc($msg);
174 break;
175 }
176 return $msg;
177 }
178
179 /**
180 * @param string $type
181 * 'raw'|'ezc'.
182 *
183 * @throws CRM_Core_Exception
184 *
185 * @return array(ezcMail)|array(string)
186 */
187 public function getAllMessages($type = 'raw') {
188 $msgs = [];
189
190 if ($this->_webtest) {
191 throw new CRM_Core_Exception('Not implemented: getAllMessages for WebTest');
192 }
193 $dao = CRM_Core_DAO::executeQuery('SELECT headers, body FROM civicrm_mailing_spool ORDER BY id');
194 while ($dao->fetch()) {
195 $msgs[] = $dao->headers . "\n\n" . $dao->body;
196 }
197
198 switch ($type) {
199 case 'raw':
200 // nothing to do
201 break;
202
203 case 'ezc':
204 foreach ($msgs as $i => $msg) {
205 $msgs[$i] = $this->convertToEzc($msg);
206 }
207 break;
208 }
209
210 return $msgs;
211 }
212
213 /**
214 * @return int
215 */
216 public function getSelectedOutboundOption() {
217 $selectedOption = CRM_Mailing_Config::OUTBOUND_OPTION_MAIL;
218 // Is there a better way to do this? How do you get the currently selected value of a radio button in selenium?
219 for ($i = 0; $i <= 5; $i++) {
220 if ($i != CRM_Mailing_Config::OUTBOUND_OPTION_MOCK) {
221 if ($this->_ut->getValue('xpath=//input[@name="outBound_option" and @value="' . $i . '"]') == "on") {
222 $selectedOption = $i;
223 break;
224 }
225 }
226 }
227 return $selectedOption;
228 }
229
230 /*
231 * Utility functions (previously part of CiviUnitTestCase)
232 * Included for backward compatibility with existing tests.
233 */
234
235 /**
236 * Check contents of mail log.
237 *
238 * @param array $strings
239 * Strings that should be included.
240 * @param array $absentStrings
241 * Strings that should not be included.
242 * @param string $prefix
243 *
244 * @return \ezcMail|string
245 */
246 public function checkMailLog($strings, $absentStrings = [], $prefix = '') {
247 $mail = $this->getMostRecentEmail('raw');
248 return $this->checkMailForStrings($strings, $absentStrings, $prefix, $mail);
249 }
250
251 /**
252 * Check contents of mail log.
253 *
254 * @param array $strings
255 * Strings that should be included.
256 * @param array $absentStrings
257 * Strings that should not be included.
258 * @param string $prefix
259 *
260 * @return \ezcMail|string
261 */
262 public function checkAllMailLog($strings, $absentStrings = [], $prefix = '') {
263 $mails = $this->getAllMessages('raw');
264 $mail = implode(',', $mails);
265 return $this->checkMailForStrings($strings, $absentStrings, $prefix, $mail);
266 }
267
268 /**
269 * Check that mail log is empty.
270 * @param string $prefix
271 */
272 public function assertMailLogEmpty($prefix = '') {
273 $mail = $this->getMostRecentEmail('raw');
274 $this->_ut->assertEmpty($mail, 'mail sent when it should not have been ' . $prefix);
275 }
276
277 /**
278 * Assert that $expectedRecipients (and no else) have received emails
279 *
280 * @param array $expectedRecipients
281 * Array($msgPos => array($recipPos => $emailAddr)).
282 */
283 public function assertRecipients($expectedRecipients) {
284 $recipients = [];
285 foreach ($this->getAllMessages('ezc') as $message) {
286 $recipients[] = CRM_Utils_Array::collect('email', $message->to);
287 }
288 $cmp = function($a, $b) {
289 if ($a[0] == $b[0]) {
290 return 0;
291 }
292 return ($a[0] < $b[0]) ? 1 : -1;
293 };
294 usort($recipients, $cmp);
295 usort($expectedRecipients, $cmp);
296 $this->_ut->assertEquals(
297 $expectedRecipients,
298 $recipients,
299 "Incorrect recipients: " . print_r(array('expected' => $expectedRecipients, 'actual' => $recipients), TRUE)
300 );
301 }
302
303 /**
304 * Assert that $expectedSubjects (and no other subjects) were sent.
305 *
306 * @param array $expectedSubjects
307 * Array(string $subj).
308 */
309 public function assertSubjects($expectedSubjects) {
310 $subjects = [];
311 foreach ($this->getAllMessages('ezc') as $message) {
312 /** @var ezcMail $message */
313 $subjects[] = $message->subject;
314 }
315 sort($subjects);
316 sort($expectedSubjects);
317 $this->_ut->assertEquals(
318 $expectedSubjects,
319 $subjects,
320 "Incorrect subjects: " . print_r(array('expected' => $expectedSubjects, 'actual' => $subjects), TRUE)
321 );
322 }
323
324 /**
325 * Remove any sent messages from the log.
326 *
327 * @param int $limit
328 * How many recent messages to remove, defaults to 0 (all).
329 *
330 * @throws \CRM_Core_Exception
331 */
332 public function clearMessages($limit = 0) {
333 if ($this->_webtest) {
334 throw new \CRM_Core_Exception("Not implemented: clearMessages for WebTest");
335 }
336 else {
337 $sql = 'DELETE FROM civicrm_mailing_spool ORDER BY id DESC';
338 if ($limit) {
339 $sql .= ' LIMIT ' . $limit;
340 }
341 CRM_Core_DAO::executeQuery($sql);
342 }
343 }
344
345 /**
346 * @param string $msg
347 * Email header and body.
348 * @return ezcMail
349 */
350 private function convertToEzc($msg) {
351 $set = new ezcMailVariableSet($msg);
352 $parser = new ezcMailParser();
353 $mail = $parser->parseMail($set);
354 $this->_ut->assertNotEmpty($mail, 'Cannot parse mail');
355 return $mail[0];
356 }
357
358 /**
359 * @param array $strings
360 * @param $absentStrings
361 * @param $prefix
362 * @param $mail
363 * @return mixed
364 */
365 public function checkMailForStrings(array $strings, $absentStrings, $prefix, $mail) {
366 foreach ($strings as $string) {
367 $this->_ut->assertStringContainsString($string, $mail, "$string . not found in $mail $prefix");
368 }
369 foreach ($absentStrings as $string) {
370 $this->_ut->assertEmpty(strstr($mail, $string), "$string incorrectly found in $mail $prefix");
371 }
372 return $mail;
373 }
374
375 }