| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | CiviCRM version 4.6 | |
| 5 | +--------------------------------------------------------------------+ |
| 6 | | Copyright CiviCRM LLC (c) 2004-2015 | |
| 7 | +--------------------------------------------------------------------+ |
| 8 | | This file is a part of CiviCRM. | |
| 9 | | | |
| 10 | | CiviCRM is free software; you can copy, modify, and distribute it | |
| 11 | | under the terms of the GNU Affero General Public License | |
| 12 | | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | |
| 13 | | | |
| 14 | | CiviCRM is distributed in the hope that it will be useful, but | |
| 15 | | WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 16 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
| 17 | | See the GNU Affero General Public License for more details. | |
| 18 | | | |
| 19 | | You should have received a copy of the GNU Affero General Public | |
| 20 | | License along with this program; if not, contact CiviCRM LLC | |
| 21 | | at info[AT]civicrm[DOT]org. If you have questions about the | |
| 22 | | GNU Affero General Public License or the licensing of CiviCRM, | |
| 23 | | see the CiviCRM license FAQ at http://civicrm.org/licensing | |
| 24 | +--------------------------------------------------------------------+ |
| 25 | */ |
| 26 | |
| 27 | /** |
| 28 | * Mail utils for use during unit testing to allow retrieval |
| 29 | * and examination of 'sent' emails. |
| 30 | * |
| 31 | * Basic usage: |
| 32 | * |
| 33 | * $mut = new CiviMailUtils( $this, true ); //true automatically starts spooling |
| 34 | * ... do stuff ... |
| 35 | * $msg = $mut->getMostRecentEmail( 'raw' ); // or 'ezc' to get an ezc mail object |
| 36 | * ... assert stuff about $msg ... |
| 37 | * $mut->stop(); |
| 38 | * |
| 39 | * |
| 40 | * @package CiviCRM |
| 41 | */ |
| 42 | |
| 43 | require_once 'ezc/Base/src/ezc_bootstrap.php'; |
| 44 | require_once 'ezc/autoload/mail_autoload.php'; |
| 45 | |
| 46 | /** |
| 47 | * Class CiviMailUtils |
| 48 | */ |
| 49 | class CiviMailUtils extends PHPUnit_Framework_TestCase { |
| 50 | |
| 51 | /** |
| 52 | * @var mixed current outbound email option |
| 53 | */ |
| 54 | protected $_outBound_option = NULL; |
| 55 | |
| 56 | /** |
| 57 | * @var bool is this a webtest |
| 58 | */ |
| 59 | protected $_webtest = FALSE; |
| 60 | |
| 61 | /** |
| 62 | * Constructor. |
| 63 | * |
| 64 | * @param CiviSeleniumTestCase|CiviUnitTestCase $unit_test The currently running test |
| 65 | * @param bool $startImmediately |
| 66 | * Start writing to db now or wait until start() is called. |
| 67 | */ |
| 68 | public function __construct(&$unit_test, $startImmediately = TRUE) { |
| 69 | $this->_ut = $unit_test; |
| 70 | |
| 71 | // Check if running under webtests or not |
| 72 | if (is_subclass_of($unit_test, 'CiviSeleniumTestCase')) { |
| 73 | $this->_webtest = TRUE; |
| 74 | } |
| 75 | |
| 76 | if ($startImmediately) { |
| 77 | $this->start(); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Start writing emails to db instead of current option. |
| 83 | */ |
| 84 | public function start() { |
| 85 | if ($this->_webtest) { |
| 86 | // Change outbound mail setting |
| 87 | $this->_ut->openCiviPage('admin/setting/smtp', "reset=1", "_qf_Smtp_next"); |
| 88 | |
| 89 | // First remember the current setting |
| 90 | $this->_outBound_option = $this->getSelectedOutboundOption(); |
| 91 | |
| 92 | $this->_ut->click('xpath=//input[@name="outBound_option" and @value="' . CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB . '"]'); |
| 93 | $this->_ut->clickLink("_qf_Smtp_next"); |
| 94 | |
| 95 | // Is there supposed to be a status message displayed when outbound email settings are changed? |
| 96 | // assert something? |
| 97 | |
| 98 | } |
| 99 | else { |
| 100 | |
| 101 | // save current setting for outbound option, then change it |
| 102 | $mailingBackend = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, |
| 103 | 'mailing_backend' |
| 104 | ); |
| 105 | |
| 106 | $this->_outBound_option = $mailingBackend['outBound_option']; |
| 107 | $mailingBackend['outBound_option'] = CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB; |
| 108 | |
| 109 | CRM_Core_BAO_Setting::setItem($mailingBackend, |
| 110 | CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, |
| 111 | 'mailing_backend' |
| 112 | ); |
| 113 | |
| 114 | $mailingBackend = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, |
| 115 | 'mailing_backend' |
| 116 | ); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | public function stop() { |
| 121 | if ($this->_webtest) { |
| 122 | if ($this->_outBound_option != CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB) { |
| 123 | // Change outbound mail setting |
| 124 | $this->_ut->openCiviPage('admin/setting/smtp', "reset=1", "_qf_Smtp_next"); |
| 125 | $this->_ut->click('xpath=//input[@name="outBound_option" and @value="' . $this->_outBound_option . '"]'); |
| 126 | // There will be a warning when switching from test to live mode |
| 127 | if ($this->_outBound_option != CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED) { |
| 128 | $this->_ut->getAlert(); |
| 129 | } |
| 130 | $this->_ut->clickLink("_qf_Smtp_next"); |
| 131 | } |
| 132 | } |
| 133 | else { |
| 134 | |
| 135 | $mailingBackend = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, |
| 136 | 'mailing_backend' |
| 137 | ); |
| 138 | |
| 139 | $mailingBackend['outBound_option'] = $this->_outBound_option; |
| 140 | |
| 141 | CRM_Core_BAO_Setting::setItem($mailingBackend, |
| 142 | CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, |
| 143 | 'mailing_backend' |
| 144 | ); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * @param string $type |
| 150 | * |
| 151 | * @return ezcMail|string |
| 152 | */ |
| 153 | public function getMostRecentEmail($type = 'raw') { |
| 154 | $msg = ''; |
| 155 | |
| 156 | if ($this->_webtest) { |
| 157 | // I don't understand but for some reason we have to load the page twice for a recent mailing to appear. |
| 158 | $this->_ut->openCiviPage('mailing/browse/archived', 'reset=1'); |
| 159 | $this->_ut->openCiviPage('mailing/browse/archived', 'reset=1', 'css=td.crm-mailing-name'); |
| 160 | } |
| 161 | // We can't fetch mailing headers from webtest so we'll only try if the format is raw |
| 162 | if ($this->_webtest && $type == 'raw') { |
| 163 | // 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. |
| 164 | $this->_ut->clickLink('xpath=//tr[contains(@id, "crm-mailing_")]//a[text()="Report"]'); |
| 165 | |
| 166 | // Also not sure how robust this is, but there isn't a good |
| 167 | // identifier for this link either. |
| 168 | $this->_ut->waitForElementPresent('xpath=//a[contains(text(), "View complete message")]'); |
| 169 | $this->_ut->clickAjaxLink('xpath=//a[contains(text(), "View complete message")]'); |
| 170 | $msg = $this->_ut->getText('css=.ui-dialog-content.crm-ajax-container'); |
| 171 | } |
| 172 | else { |
| 173 | $dao = CRM_Core_DAO::executeQuery('SELECT headers, body FROM civicrm_mailing_spool ORDER BY id DESC LIMIT 1'); |
| 174 | if ($dao->fetch()) { |
| 175 | $msg = $dao->headers . "\n\n" . $dao->body; |
| 176 | } |
| 177 | $dao->free(); |
| 178 | } |
| 179 | |
| 180 | switch ($type) { |
| 181 | case 'raw': |
| 182 | // nothing to do |
| 183 | break; |
| 184 | |
| 185 | case 'ezc': |
| 186 | $msg = $this->convertToEzc($msg); |
| 187 | break; |
| 188 | } |
| 189 | return $msg; |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * @param string $type |
| 194 | * 'raw'|'ezc'. |
| 195 | * |
| 196 | * @throws Exception |
| 197 | * @return array(ezcMail)|array(string) |
| 198 | */ |
| 199 | public function getAllMessages($type = 'raw') { |
| 200 | $msgs = array(); |
| 201 | |
| 202 | if ($this->_webtest) { |
| 203 | throw new Exception("Not implementated: getAllMessages for WebTest"); |
| 204 | } |
| 205 | else { |
| 206 | $dao = CRM_Core_DAO::executeQuery('SELECT headers, body FROM civicrm_mailing_spool ORDER BY id'); |
| 207 | while ($dao->fetch()) { |
| 208 | $msgs[] = $dao->headers . "\n\n" . $dao->body; |
| 209 | } |
| 210 | $dao->free(); |
| 211 | } |
| 212 | |
| 213 | switch ($type) { |
| 214 | case 'raw': |
| 215 | // nothing to do |
| 216 | break; |
| 217 | |
| 218 | case 'ezc': |
| 219 | foreach ($msgs as $i => $msg) { |
| 220 | $msgs[$i] = $this->convertToEzc($msg); |
| 221 | } |
| 222 | break; |
| 223 | } |
| 224 | |
| 225 | return $msgs; |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * @return int |
| 230 | */ |
| 231 | public function getSelectedOutboundOption() { |
| 232 | $selectedOption = CRM_Mailing_Config::OUTBOUND_OPTION_MAIL; |
| 233 | // Is there a better way to do this? How do you get the currently selected value of a radio button in selenium? |
| 234 | for ($i = 0; $i <= 5; $i++) { |
| 235 | if ($i != CRM_Mailing_Config::OUTBOUND_OPTION_MOCK) { |
| 236 | if ($this->_ut->getValue('xpath=//input[@name="outBound_option" and @value="' . $i . '"]') == "on") { |
| 237 | $selectedOption = $i; |
| 238 | break; |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | return $selectedOption; |
| 243 | } |
| 244 | |
| 245 | /* |
| 246 | * Utility functions (previously part of CiviUnitTestCase) |
| 247 | * Included for backward compatibility with existing tests. |
| 248 | */ |
| 249 | |
| 250 | /** |
| 251 | * Check contents of mail log. |
| 252 | * |
| 253 | * @param array $strings |
| 254 | * Strings that should be included. |
| 255 | * @param array $absentStrings |
| 256 | * Strings that should not be included. |
| 257 | * @param string $prefix |
| 258 | * |
| 259 | * @return \ezcMail|string |
| 260 | */ |
| 261 | public function checkMailLog($strings, $absentStrings = array(), $prefix = '') { |
| 262 | $mail = $this->getMostRecentEmail('raw'); |
| 263 | foreach ($strings as $string) { |
| 264 | $this->_ut->assertContains($string, $mail, "$string . not found in $mail $prefix"); |
| 265 | } |
| 266 | foreach ($absentStrings as $string) { |
| 267 | $this->_ut->assertEmpty(strstr($mail, $string), "$string incorrectly found in $mail $prefix");; |
| 268 | } |
| 269 | return $mail; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Check that mail log is empty. |
| 274 | * @param string $prefix |
| 275 | */ |
| 276 | public function assertMailLogEmpty($prefix = '') { |
| 277 | $mail = $this->getMostRecentEmail('raw'); |
| 278 | $this->_ut->assertEmpty($mail, 'mail sent when it should not have been ' . $prefix); |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Assert that $expectedRecipients (and no else) have received emails |
| 283 | * |
| 284 | * @param array $expectedRecipients |
| 285 | * Array($msgPos => array($recipPos => $emailAddr)). |
| 286 | */ |
| 287 | public function assertRecipients($expectedRecipients) { |
| 288 | $recipients = array(); |
| 289 | foreach ($this->getAllMessages('ezc') as $message) { |
| 290 | $recipients[] = CRM_Utils_Array::collect('email', $message->to); |
| 291 | } |
| 292 | sort($recipients); |
| 293 | sort($expectedRecipients); |
| 294 | $this->_ut->assertEquals( |
| 295 | $expectedRecipients, |
| 296 | $recipients, |
| 297 | "Incorrect recipients: " . print_r(array('expected' => $expectedRecipients, 'actual' => $recipients), TRUE) |
| 298 | ); |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Remove any sent messages from the log. |
| 303 | */ |
| 304 | public function clearMessages() { |
| 305 | if ($this->_webtest) { |
| 306 | throw new Exception("Not implementated: clearMessages for WebTest"); |
| 307 | } |
| 308 | else { |
| 309 | CRM_Core_DAO::executeQuery('DELETE FROM civicrm_mailing_spool ORDER BY id DESC LIMIT 1'); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * @param string $msg |
| 315 | * Email header and body. |
| 316 | * @return ezcMail |
| 317 | */ |
| 318 | private function convertToEzc($msg) { |
| 319 | $set = new ezcMailVariableSet($msg); |
| 320 | $parser = new ezcMailParser(); |
| 321 | $mail = $parser->parseMail($set); |
| 322 | $this->_ut->assertNotEmpty($mail, 'Cannot parse mail'); |
| 323 | return $mail[0]; |
| 324 | } |
| 325 | |
| 326 | } |