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