Merge branch '5.11' of https://github.com/civicrm/civicrm-core
[civicrm-core.git] / tests / phpunit / api / v3 / JobProcessMailingTest.php
CommitLineData
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 */
44class 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}