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