Commit | Line | Data |
---|---|---|
07c09ae4 EM |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
2fe49090 | 4 | | CiviCRM version 5 | |
07c09ae4 | 5 | +--------------------------------------------------------------------+ |
6b83d5bd | 6 | | Copyright CiviCRM LLC (c) 2004-2019 | |
07c09ae4 EM |
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 and the CiviCRM Licensing Exception along | | |
21 | | with this program; if not, contact CiviCRM LLC | | |
22 | | at info[AT]civicrm[DOT]org. If you have questions about the | | |
23 | | GNU Affero General Public License or the licensing of CiviCRM, | | |
24 | | see the CiviCRM license FAQ at http://civicrm.org/licensing | | |
25 | +--------------------------------------------------------------------+ | |
26 | */ | |
27 | ||
28 | /** | |
29 | * File for the CiviCRM APIv3 job functions | |
30 | * | |
31 | * @package CiviCRM_APIv3 | |
32 | * @subpackage API_Job | |
33 | * | |
6b83d5bd | 34 | * @copyright CiviCRM LLC (c) 2004-2019 |
07c09ae4 EM |
35 | * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $ |
36 | * | |
37 | */ | |
0eea664b | 38 | |
07c09ae4 EM |
39 | /** |
40 | * Class api_v3_JobTest | |
acb109b7 | 41 | * @group headless |
b4a332a9 | 42 | * @group civimail |
07c09ae4 EM |
43 | */ |
44 | class api_v3_JobProcessMailingTest extends CiviUnitTestCase { | |
45 | protected $_apiversion = 3; | |
46 | ||
47 | public $DBResetRequired = FALSE; | |
48 | public $_entity = 'Job'; | |
49 | public $_params = array(); | |
50 | private $_groupID; | |
51 | private $_email; | |
52 | ||
d667a9ba TO |
53 | protected $defaultSettings; |
54 | ||
07c09ae4 EM |
55 | /** |
56 | * @var CiviMailUtils | |
57 | */ | |
58 | private $_mut; | |
59 | ||
00be9182 | 60 | public function setUp() { |
d667a9ba | 61 | $this->cleanupMailingTest(); |
07c09ae4 | 62 | parent::setUp(); |
97b7d4a0 | 63 | CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW |
07c09ae4 EM |
64 | $this->_groupID = $this->groupCreate(); |
65 | $this->_email = 'test@test.test'; | |
66 | $this->_params = array( | |
67 | 'subject' => 'Accidents in cars cause children', | |
21b09c13 | 68 | 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.', |
07c09ae4 EM |
69 | 'name' => 'mailing name', |
70 | 'created_id' => 1, | |
71 | 'groups' => array('include' => array($this->_groupID)), | |
5f445749 | 72 | 'scheduled_date' => 'now', |
07c09ae4 | 73 | ); |
d667a9ba | 74 | $this->defaultSettings = array( |
502eefb0 | 75 | 'mailings' => 1, // int, #mailings to send |
d667a9ba TO |
76 | 'recipients' => 20, // int, #contacts to receive mailing |
77 | 'workers' => 1, // int, #concurrent cron jobs | |
78 | 'iterations' => 1, // int, #times to spawn all the workers | |
79 | 'lockHold' => 0, // int, #extra seconds each cron job should hold lock | |
80 | 'mailerBatchLimit' => 0, // int, max# recipients to send in a given cron run | |
81 | 'mailerJobsMax' => 0, // int, max# concurrent jobs | |
82 | 'mailerJobSize' => 0, // int, max# recipients in each job | |
83 | 'mailThrottleTime' => 0, // int, microseconds separating messages | |
84 | ); | |
481a74f4 | 85 | $this->_mut = new CiviMailUtils($this, TRUE); |
07c09ae4 EM |
86 | $this->callAPISuccess('mail_settings', 'get', array('api.mail_settings.create' => array('domain' => 'chaos.org'))); |
87 | } | |
88 | ||
89 | /** | |
07c09ae4 | 90 | */ |
00be9182 | 91 | public function tearDown() { |
d667a9ba | 92 | //$this->_mut->clearMessages(); |
07c09ae4 | 93 | $this->_mut->stop(); |
07c09ae4 | 94 | CRM_Utils_Hook::singleton()->reset(); |
97b7d4a0 | 95 | CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW |
d667a9ba | 96 | //$this->cleanupMailingTest(); |
07c09ae4 | 97 | parent::tearDown(); |
07c09ae4 EM |
98 | } |
99 | ||
d667a9ba | 100 | public function testBasic() { |
07c09ae4 | 101 | $this->createContactsInGroup(10, $this->_groupID); |
dc00ac6d | 102 | Civi::settings()->add(array( |
d667a9ba TO |
103 | 'mailerBatchLimit' => 2, |
104 | )); | |
07c09ae4 | 105 | $this->callAPISuccess('mailing', 'create', $this->_params); |
d667a9ba | 106 | $this->_mut->assertRecipients(array()); |
07c09ae4 EM |
107 | $this->callAPISuccess('job', 'process_mailing', array()); |
108 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
4aa8f804 JP |
109 | } |
110 | ||
111 | /** | |
112 | * Test pause and resume on Mailing. | |
113 | */ | |
114 | public function testPauseAndResumeMailing() { | |
115 | $this->createContactsInGroup(10, $this->_groupID); | |
116 | Civi::settings()->add(array( | |
117 | 'mailerBatchLimit' => 2, | |
118 | )); | |
67d4ed51 | 119 | $this->_mut->clearMessages(); |
4aa8f804 JP |
120 | //Create a test mailing and check if the status is set to Scheduled. |
121 | $result = $this->callAPISuccess('mailing', 'create', $this->_params); | |
122 | $jobs = $this->callAPISuccess('mailing_job', 'get', array('mailing_id' => $result['id'])); | |
123 | $this->assertEquals('Scheduled', $jobs['values'][$jobs['id']]['status']); | |
124 | ||
67d4ed51 | 125 | //Pause the mailing. |
4aa8f804 JP |
126 | CRM_Mailing_BAO_MailingJob::pause($result['id']); |
127 | $jobs = $this->callAPISuccess('mailing_job', 'get', array('mailing_id' => $result['id'])); | |
128 | $this->assertEquals('Paused', $jobs['values'][$jobs['id']]['status']); | |
129 | ||
130 | //Verify if Paused mailing isn't considered in process_mailing job. | |
131 | $this->callAPISuccess('job', 'process_mailing', array()); | |
67d4ed51 JP |
132 | //Check if mail log is empty. |
133 | $this->_mut->assertMailLogEmpty(); | |
4aa8f804 JP |
134 | $jobs = $this->callAPISuccess('mailing_job', 'get', array('mailing_id' => $result['id'])); |
135 | $this->assertEquals('Paused', $jobs['values'][$jobs['id']]['status']); | |
136 | ||
137 | //Resume should set the status back to Scheduled. | |
138 | CRM_Mailing_BAO_MailingJob::resume($result['id']); | |
139 | $jobs = $this->callAPISuccess('mailing_job', 'get', array('mailing_id' => $result['id'])); | |
140 | $this->assertEquals('Scheduled', $jobs['values'][$jobs['id']]['status']); | |
67d4ed51 JP |
141 | |
142 | //Execute the job and it should send the mailing to the recipients now. | |
143 | $this->callAPISuccess('job', 'process_mailing', array()); | |
144 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
07c09ae4 EM |
145 | } |
146 | ||
f008885c E |
147 | /** |
148 | * Test mail when in non-production environment. | |
149 | * | |
150 | */ | |
151 | public function testMailNonProductionRun() { | |
152 | // Test in non-production mode. | |
153 | $params = array( | |
154 | 'environment' => 'Staging', | |
155 | ); | |
156 | $this->callAPISuccess('Setting', 'create', $params); | |
288e5f75 JP |
157 | //Assert if outbound mail is disabled. |
158 | $mailingBackend = Civi::settings()->get('mailing_backend'); | |
159 | $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED); | |
160 | ||
f008885c E |
161 | $this->createContactsInGroup(10, $this->_groupID); |
162 | Civi::settings()->add(array( | |
163 | 'mailerBatchLimit' => 2, | |
164 | )); | |
165 | $this->callAPISuccess('mailing', 'create', $this->_params); | |
166 | $this->_mut->assertRecipients(array()); | |
167 | $this->callAPIFailure('job', 'process_mailing', "Failure in api call for job process_mailing: Job has not been executed as it is a non-production environment."); | |
168 | ||
169 | // Test with runInNonProductionEnvironment param. | |
170 | $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE)); | |
171 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
172 | ||
288e5f75 JP |
173 | $jobId = $this->callAPISuccessGetValue('Job', array( |
174 | 'return' => "id", | |
175 | 'api_action' => "group_rebuild", | |
176 | )); | |
177 | $this->callAPISuccess('Job', 'create', array( | |
178 | 'id' => $jobId, | |
179 | 'parameters' => "runInNonProductionEnvironment=TRUE", | |
180 | )); | |
181 | $jobManager = new CRM_Core_JobManager(); | |
182 | $jobManager->executeJobById($jobId); | |
183 | ||
184 | //Assert if outbound mail is still disabled. | |
185 | $mailingBackend = Civi::settings()->get('mailing_backend'); | |
186 | $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED); | |
187 | ||
f008885c E |
188 | // Test in production mode. |
189 | $params = array( | |
190 | 'environment' => 'Production', | |
191 | ); | |
192 | $this->callAPISuccess('Setting', 'create', $params); | |
193 | $this->callAPISuccess('job', 'process_mailing', array()); | |
194 | $this->_mut->assertRecipients($this->getRecipients(1, 2)); | |
195 | } | |
196 | ||
d667a9ba TO |
197 | public function concurrencyExamples() { |
198 | $es = array(); | |
199 | ||
200 | // Launch 3 workers, but mailerJobsMax limits us to 1 worker. | |
201 | $es[0] = array( | |
202 | array( | |
203 | 'recipients' => 20, | |
204 | 'workers' => 3, | |
a2341a99 TO |
205 | // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because |
206 | // the data.* locks trample the worker.* locks. However, setting lockHold allows us to | |
207 | // approximate the behavior of what would happen *if* the lock-implementation didn't suffer | |
208 | // trampling effects. | |
d667a9ba TO |
209 | 'lockHold' => 10, |
210 | 'mailerBatchLimit' => 4, | |
211 | 'mailerJobsMax' => 1, | |
212 | ), | |
213 | array( | |
214 | 0 => 2, // 2 jobs which produce 0 messages | |
215 | 4 => 1, // 1 job which produces 4 messages | |
216 | ), | |
217 | 4, | |
218 | ); | |
219 | ||
220 | // Launch 3 workers, but mailerJobsMax limits us to 2 workers. | |
221 | $es[1] = array( | |
222 | array(// Settings. | |
223 | 'recipients' => 20, | |
224 | 'workers' => 3, | |
a2341a99 TO |
225 | // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because |
226 | // the data.* locks trample the worker.* locks. However, setting lockHold allows us to | |
227 | // approximate the behavior of what would happen *if* the lock-implementation didn't suffer | |
228 | // trampling effects. | |
d667a9ba TO |
229 | 'lockHold' => 10, |
230 | 'mailerBatchLimit' => 5, | |
231 | 'mailerJobsMax' => 2, | |
232 | ), | |
233 | array(// Tallies. | |
234 | 0 => 1, // 1 job which produce 0 messages | |
235 | 5 => 2, // 2 jobs which produce 5 messages | |
236 | ), | |
237 | 10, // Total sent. | |
238 | ); | |
239 | ||
240 | // Launch 3 workers and saturate them (mailerJobsMax=3) | |
241 | $es[2] = array( | |
242 | array(// Settings. | |
243 | 'recipients' => 20, | |
244 | 'workers' => 3, | |
d667a9ba TO |
245 | 'mailerBatchLimit' => 6, |
246 | 'mailerJobsMax' => 3, | |
247 | ), | |
248 | array(// Tallies. | |
249 | 6 => 3, // 3 jobs which produce 6 messages | |
250 | ), | |
251 | 18, // Total sent. | |
252 | ); | |
253 | ||
254 | // Launch 4 workers and saturate them (mailerJobsMax=0) | |
255 | $es[3] = array( | |
256 | array(// Settings. | |
257 | 'recipients' => 20, | |
258 | 'workers' => 4, | |
d667a9ba TO |
259 | 'mailerBatchLimit' => 6, |
260 | 'mailerJobsMax' => 0, | |
261 | ), | |
262 | array(// Tallies. | |
263 | 6 => 3, // 3 jobs which produce 6 messages | |
264 | 2 => 1, // 1 job which produces 2 messages | |
265 | ), | |
266 | 20, // Total sent. | |
267 | ); | |
268 | ||
269 | // Launch 1 worker, 3 times in a row. Deliver everything. | |
270 | $es[4] = array( | |
271 | array(// Settings. | |
272 | 'recipients' => 10, | |
273 | 'workers' => 1, | |
274 | 'iterations' => 3, | |
275 | 'mailerBatchLimit' => 7, | |
276 | ), | |
277 | array(// Tallies. | |
278 | 7 => 1, // 1 job which produces 7 messages | |
279 | 3 => 1, // 1 job which produces 3 messages | |
280 | 0 => 1, // 1 job which produces 0 messages | |
281 | ), | |
282 | 10, // Total sent. | |
283 | ); | |
284 | ||
285 | // Launch 2 worker, 3 times in a row. Deliver everything. | |
286 | $es[5] = array( | |
287 | array(// Settings. | |
288 | 'recipients' => 10, | |
289 | 'workers' => 2, | |
290 | 'iterations' => 3, | |
291 | 'mailerBatchLimit' => 3, | |
292 | ), | |
293 | array(// Tallies. | |
294 | 3 => 3, // 3 jobs which produce 3 messages | |
295 | 1 => 1, // 1 job which produces 1 messages | |
296 | 0 => 2, // 2 jobs which produce 0 messages | |
297 | ), | |
298 | 10, // Total sent. | |
299 | ); | |
300 | ||
502eefb0 TO |
301 | // For two mailings, launch 1 worker, 5 times in a row. Deliver everything. |
302 | $es[6] = array( | |
303 | array(// Settings. | |
304 | 'mailings' => 2, | |
305 | 'recipients' => 10, | |
306 | 'workers' => 1, | |
307 | 'iterations' => 5, | |
308 | 'mailerBatchLimit' => 6, | |
309 | ), | |
310 | array(// Tallies. | |
311 | // x6 => x4+x2 => x6 => x2 => x0 | |
312 | 6 => 3, // 3 jobs which produce 6 messages | |
313 | 2 => 1, // 1 job which produces 2 messages | |
314 | 0 => 1, // 1 job which produces 0 messages | |
315 | ), | |
316 | 20, // Total sent. | |
317 | ); | |
318 | ||
d667a9ba TO |
319 | return $es; |
320 | } | |
321 | ||
322 | /** | |
323 | * Setup various mail configuration options (eg $mailerBatchLimit, | |
324 | * $mailerJobMax) and spawn multiple worker threads ($workers). | |
325 | * Allow the threads to complete. (Optionally, repeat the above | |
326 | * process.) Finally, check to see if the right number of | |
327 | * jobs delivered the right number of messages. | |
328 | * | |
329 | * @param array $settings | |
330 | * An array of settings (eg mailerBatchLimit, workers). See comments | |
331 | * for $this->defaultSettings. | |
332 | * @param array $expectedTallies | |
333 | * A listing of the number cron-runs keyed by their size. | |
334 | * For example, array(10=>2) means that there 2 cron-runs | |
335 | * which delivered 10 messages each. | |
336 | * @param int $expectedTotal | |
337 | * The total number of contacts for whom messages should have | |
338 | * been sent. | |
339 | * @dataProvider concurrencyExamples | |
340 | */ | |
341 | public function testConcurrency($settings, $expectedTallies, $expectedTotal) { | |
342 | $settings = array_merge($this->defaultSettings, $settings); | |
343 | ||
344 | $this->createContactsInGroup($settings['recipients'], $this->_groupID); | |
dc00ac6d | 345 | Civi::settings()->add(CRM_Utils_Array::subset($settings, array( |
d667a9ba TO |
346 | 'mailerBatchLimit', |
347 | 'mailerJobsMax', | |
348 | 'mailThrottleTime', | |
349 | ))); | |
350 | ||
502eefb0 TO |
351 | for ($i = 0; $i < $settings['mailings']; $i++) { |
352 | $this->callAPISuccess('mailing', 'create', $this->_params); | |
353 | } | |
d667a9ba TO |
354 | |
355 | $this->_mut->assertRecipients(array()); | |
356 | ||
357 | $allApiResults = array(); | |
358 | for ($iterationId = 0; $iterationId < $settings['iterations']; $iterationId++) { | |
359 | $apiCalls = $this->createExternalAPI(); | |
360 | $apiCalls->addEnv(array('CIVICRM_CRON_HOLD' => $settings['lockHold'])); | |
361 | for ($workerId = 0; $workerId < $settings['workers']; $workerId++) { | |
362 | $apiCalls->addCall('job', 'process_mailing', array()); | |
363 | } | |
364 | $apiCalls->start(); | |
365 | $this->assertEquals($settings['workers'], $apiCalls->getRunningCount()); | |
366 | ||
367 | $apiCalls->wait(); | |
368 | $allApiResults = array_merge($allApiResults, $apiCalls->getResults()); | |
369 | } | |
370 | ||
371 | $actualTallies = $this->tallyApiResults($allApiResults); | |
372 | $this->assertEquals($expectedTallies, $actualTallies, 'API tallies should match.' . print_r(array( | |
373 | 'expectedTallies' => $expectedTallies, | |
374 | 'actualTallies' => $actualTallies, | |
375 | 'apiResults' => $allApiResults, | |
376 | ), TRUE)); | |
502eefb0 | 377 | $this->_mut->assertRecipients($this->getRecipients(1, $expectedTotal / $settings['mailings'], 'nul.example.com', $settings['mailings'])); |
d667a9ba TO |
378 | $this->assertEquals(0, $apiCalls->getRunningCount()); |
379 | } | |
380 | ||
07c09ae4 | 381 | /** |
54957108 | 382 | * Create contacts in group. |
383 | * | |
e16033b4 TO |
384 | * @param int $count |
385 | * @param int $groupID | |
54957108 | 386 | * @param string $domain |
07c09ae4 | 387 | */ |
d667a9ba | 388 | public function createContactsInGroup($count, $groupID, $domain = 'nul.example.com') { |
481a74f4 | 389 | for ($i = 1; $i <= $count; $i++) { |
d667a9ba | 390 | $contactID = $this->individualCreate(array('first_name' => $count, 'email' => 'mail' . $i . '@' . $domain)); |
92915c55 | 391 | $this->callAPISuccess('group_contact', 'create', array( |
d667a9ba TO |
392 | 'contact_id' => $contactID, |
393 | 'group_id' => $groupID, | |
394 | 'status' => 'Added', | |
395 | )); | |
07c09ae4 EM |
396 | } |
397 | } | |
398 | ||
399 | /** | |
d667a9ba TO |
400 | * Construct the list of email addresses for $count recipients. |
401 | * | |
e16033b4 TO |
402 | * @param int $start |
403 | * @param int $count | |
502eefb0 TO |
404 | * @param string $domain |
405 | * @param int $mailings | |
07c09ae4 EM |
406 | * |
407 | * @return array | |
408 | */ | |
502eefb0 | 409 | public function getRecipients($start, $count, $domain = 'nul.example.com', $mailings = 1) { |
07c09ae4 | 410 | $recipients = array(); |
502eefb0 TO |
411 | for ($m = 0; $m < $mailings; $m++) { |
412 | for ($i = $start; $i < ($start + $count); $i++) { | |
413 | $recipients[][0] = 'mail' . $i . '@' . $domain; | |
414 | } | |
07c09ae4 EM |
415 | } |
416 | return $recipients; | |
417 | } | |
96025800 | 418 | |
d667a9ba TO |
419 | protected function cleanupMailingTest() { |
420 | $this->quickCleanup(array( | |
421 | 'civicrm_mailing', | |
422 | 'civicrm_mailing_job', | |
423 | 'civicrm_mailing_spool', | |
424 | 'civicrm_mailing_group', | |
425 | 'civicrm_mailing_recipients', | |
426 | 'civicrm_mailing_event_queue', | |
427 | 'civicrm_mailing_event_bounce', | |
428 | 'civicrm_mailing_event_delivered', | |
429 | 'civicrm_group', | |
430 | 'civicrm_group_contact', | |
431 | 'civicrm_contact', | |
432 | )); | |
433 | } | |
434 | ||
435 | /** | |
436 | * Categorize results based on (a) whether they succeeded | |
437 | * and (b) the number of messages sent. | |
438 | * | |
439 | * @param array $apiResults | |
440 | * @return array | |
441 | * One key 'error' for all failures. | |
442 | * A separate key for each distinct quantity. | |
443 | */ | |
444 | protected function tallyApiResults($apiResults) { | |
445 | $ret = array(); | |
446 | foreach ($apiResults as $apiResult) { | |
447 | $key = !empty($apiResult['is_error']) ? 'error' : $apiResult['values']['processed']; | |
448 | $ret[$key] = !empty($ret[$key]) ? 1 + $ret[$key] : 1; | |
449 | } | |
450 | return $ret; | |
451 | } | |
452 | ||
07c09ae4 | 453 | } |