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