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