3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * File for the CiviCRM APIv3 job functions
15 * @package CiviCRM_APIv3
18 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
24 * Class api_v3_JobTest
28 class api_v3_JobProcessMailingTest
extends CiviUnitTestCase
{
29 protected $_apiversion = 3;
31 public $DBResetRequired = FALSE;
32 public $_entity = 'Job';
37 protected $defaultSettings;
44 public function setUp() {
45 $this->cleanupMailingTest();
48 CRM_Mailing_BAO_MailingJob
::$mailsProcessed = 0;
49 $this->_groupID
= $this->groupCreate();
50 $this->_email
= 'test@test.test';
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',
56 'groups' => ['include' => [$this->_groupID
]],
57 'scheduled_date' => 'now',
59 $this->defaultSettings
= [
60 // int, #mailings to send
62 // int, #contacts to receive mailing
64 // int, #concurrent cron jobs
66 // int, #times to spawn all the workers
68 // int, #extra seconds each cron job should hold lock
70 // int, max# recipients to send in a given cron run
71 'mailerBatchLimit' => 0,
72 // int, max# concurrent jobs
74 // int, max# recipients in each job
76 // int, microseconds separating messages
77 'mailThrottleTime' => 0,
79 $this->_mut
= new CiviMailUtils($this, TRUE);
80 $this->callAPISuccess('mail_settings', 'get', ['api.mail_settings.create' => ['domain' => 'chaos.org']]);
85 public function tearDown() {
86 //$this->_mut->clearMessages();
88 CRM_Utils_Hook
::singleton()->reset();
90 CRM_Mailing_BAO_MailingJob
::$mailsProcessed = 0;
91 //$this->cleanupMailingTest();
95 public function testBasic() {
96 $this->createContactsInGroup(10, $this->_groupID
);
97 Civi
::settings()->add([
98 'mailerBatchLimit' => 2,
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));
107 * Test what happens when a contact is set to decesaed
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
,
116 $this->createContactsInGroup(2, $this->_groupID
);
117 Civi
::settings()->add([
118 'mailerBatchLimit' => 2,
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));
131 * Test that "multiple bulk email recipients" setting is respected.
133 public function testMultipleBulkRecipients() {
134 Civi
::settings()->add([
135 'civimail_multiple_bulk_emails' => 1,
137 $contactID = $this->individualCreate(['first_name' => 'test recipient']);
138 $email1 = $this->callAPISuccess('email', 'create', [
139 'contact_id' => $contactID,
140 'email' => 'mail1@example.org',
143 $email2 = $this->callAPISuccess('email', 'create', [
144 'contact_id' => $contactID,
145 'email' => 'mail2@example.org',
148 $this->callAPISuccess('group_contact', 'create', [
149 'contact_id' => $contactID,
150 'group_id' => $this->_groupID
,
153 $mailing = $this->callAPISuccess('mailing', 'create', $this->_params
);
154 $this->assertEquals(2, $this->callAPISuccess('MailingRecipients', 'get', ['mailing_id' => $mailing['id']])['count']);
155 $this->callAPISuccess('job', 'process_mailing', []);
156 $this->_mut
->assertRecipients([['mail1@example.org'], ['mail2@example.org']]);
157 // Don't leave data lying around for other tests to screw up on.
158 $this->callAPISuccess('Email', 'delete', ['id' => $email1['id']]);
159 $this->callAPISuccess('Email', 'delete', ['id' => $email2['id']]);
163 * Test pause and resume on Mailing.
165 public function testPauseAndResumeMailing() {
166 $this->createContactsInGroup(10, $this->_groupID
);
167 Civi
::settings()->add([
168 'mailerBatchLimit' => 2,
170 $this->_mut
->clearMessages();
171 //Create a test mailing and check if the status is set to Scheduled.
172 $result = $this->callAPISuccess('mailing', 'create', $this->_params
);
173 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
174 $this->assertEquals('Scheduled', $jobs['values'][$jobs['id']]['status']);
177 CRM_Mailing_BAO_MailingJob
::pause($result['id']);
178 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
179 $this->assertEquals('Paused', $jobs['values'][$jobs['id']]['status']);
181 //Verify if Paused mailing isn't considered in process_mailing job.
182 $this->callAPISuccess('job', 'process_mailing', []);
183 //Check if mail log is empty.
184 $this->_mut
->assertMailLogEmpty();
185 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
186 $this->assertEquals('Paused', $jobs['values'][$jobs['id']]['status']);
188 //Resume should set the status back to Scheduled.
189 CRM_Mailing_BAO_MailingJob
::resume($result['id']);
190 $jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
191 $this->assertEquals('Scheduled', $jobs['values'][$jobs['id']]['status']);
193 //Execute the job and it should send the mailing to the recipients now.
194 $this->callAPISuccess('job', 'process_mailing', []);
195 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
196 // Ensure that loading the report produces no errors.
197 $report = CRM_Mailing_BAO_Mailing
::report($result['id']);
198 // dev/mailing#56 dev/mailing#57 Ensure that for completed mailings the jobs array is not empty.
199 $this->assertTrue(!empty($report['jobs']));
200 // Ensure that mailing name is correctly stored in the report.
201 $this->assertEquals('mailing name', $report['mailing']['name']);
205 * Test mail when in non-production environment.
208 public function testMailNonProductionRun() {
209 // Test in non-production mode.
211 'environment' => 'Staging',
213 $this->callAPISuccess('Setting', 'create', $params);
214 //Assert if outbound mail is disabled.
215 $mailingBackend = Civi
::settings()->get('mailing_backend');
216 $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config
::OUTBOUND_OPTION_DISABLED
);
218 $this->createContactsInGroup(10, $this->_groupID
);
219 Civi
::settings()->add([
220 'mailerBatchLimit' => 2,
222 $this->callAPISuccess('mailing', 'create', $this->_params
);
223 $this->_mut
->assertRecipients([]);
224 $result = $this->callAPIFailure('job', 'process_mailing', []);
225 $this->assertEquals($result['error_message'], "Job has not been executed as it is a Staging (non-production) environment.");
227 // Test with runInNonProductionEnvironment param.
228 $this->callAPISuccess('job', 'process_mailing', ['runInNonProductionEnvironment' => TRUE]);
229 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
231 $jobId = $this->callAPISuccessGetValue('Job', [
233 'api_action' => "group_rebuild",
235 $this->callAPISuccess('Job', 'create', [
237 'parameters' => "runInNonProductionEnvironment=TRUE",
239 $jobManager = new CRM_Core_JobManager();
240 $jobManager->executeJobById($jobId);
242 //Assert if outbound mail is still disabled.
243 $mailingBackend = Civi
::settings()->get('mailing_backend');
244 $this->assertEquals($mailingBackend['outBound_option'], CRM_Mailing_Config
::OUTBOUND_OPTION_DISABLED
);
246 // Test in production mode.
248 'environment' => 'Production',
250 $this->callAPISuccess('Setting', 'create', $params);
251 $this->callAPISuccess('job', 'process_mailing', []);
252 $this->_mut
->assertRecipients($this->getRecipients(1, 2));
255 public function concurrencyExamples() {
258 // Launch 3 workers, but mailerJobsMax limits us to 1 worker.
263 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
264 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
265 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
266 // trampling effects.
268 'mailerBatchLimit' => 4,
269 'mailerJobsMax' => 1,
272 // 2 jobs which produce 0 messages
274 // 1 job which produces 4 messages
280 // Launch 3 workers, but mailerJobsMax limits us to 2 workers.
286 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
287 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
288 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
289 // trampling effects.
291 'mailerBatchLimit' => 5,
292 'mailerJobsMax' => 2,
296 // 1 job which produce 0 messages
298 // 2 jobs which produce 5 messages
305 // Launch 3 workers and saturate them (mailerJobsMax=3)
311 'mailerBatchLimit' => 6,
312 'mailerJobsMax' => 3,
316 // 3 jobs which produce 6 messages
323 // Launch 4 workers and saturate them (mailerJobsMax=0)
329 'mailerBatchLimit' => 6,
330 'mailerJobsMax' => 0,
334 // 3 jobs which produce 6 messages
336 // 1 job which produces 2 messages
343 // Launch 1 worker, 3 times in a row. Deliver everything.
350 'mailerBatchLimit' => 7,
354 // 1 job which produces 7 messages
356 // 1 job which produces 3 messages
358 // 1 job which produces 0 messages
365 // Launch 2 worker, 3 times in a row. Deliver everything.
372 'mailerBatchLimit' => 3,
376 // 3 jobs which produce 3 messages
378 // 1 job which produces 1 messages
380 // 2 jobs which produce 0 messages
387 // For two mailings, launch 1 worker, 5 times in a row. Deliver everything.
395 'mailerBatchLimit' => 6,
399 // x6 => x4+x2 => x6 => x2 => x0
400 // 3 jobs which produce 6 messages
402 // 1 job which produces 2 messages
404 // 1 job which produces 0 messages
415 * Setup various mail configuration options (eg $mailerBatchLimit,
416 * $mailerJobMax) and spawn multiple worker threads ($workers).
417 * Allow the threads to complete. (Optionally, repeat the above
418 * process.) Finally, check to see if the right number of
419 * jobs delivered the right number of messages.
421 * @param array $settings
422 * An array of settings (eg mailerBatchLimit, workers). See comments
423 * for $this->defaultSettings.
424 * @param array $expectedTallies
425 * A listing of the number cron-runs keyed by their size.
426 * For example, array(10=>2) means that there 2 cron-runs
427 * which delivered 10 messages each.
428 * @param int $expectedTotal
429 * The total number of contacts for whom messages should have
431 * @dataProvider concurrencyExamples
433 public function testConcurrency($settings, $expectedTallies, $expectedTotal) {
434 $settings = array_merge($this->defaultSettings
, $settings);
436 $this->createContactsInGroup($settings['recipients'], $this->_groupID
);
437 Civi
::settings()->add(CRM_Utils_Array
::subset($settings, [
443 for ($i = 0; $i < $settings['mailings']; $i++
) {
444 $this->callAPISuccess('mailing', 'create', $this->_params
);
447 $this->_mut
->assertRecipients([]);
450 for ($iterationId = 0; $iterationId < $settings['iterations']; $iterationId++
) {
451 $apiCalls = $this->createExternalAPI();
452 $apiCalls->addEnv(['CIVICRM_CRON_HOLD' => $settings['lockHold']]);
453 for ($workerId = 0; $workerId < $settings['workers']; $workerId++
) {
454 $apiCalls->addCall('job', 'process_mailing', []);
457 $this->assertEquals($settings['workers'], $apiCalls->getRunningCount());
460 $allApiResults = array_merge($allApiResults, $apiCalls->getResults());
463 $actualTallies = $this->tallyApiResults($allApiResults);
464 $this->assertEquals($expectedTallies, $actualTallies, 'API tallies should match.' . print_r([
465 'expectedTallies' => $expectedTallies,
466 'actualTallies' => $actualTallies,
467 'apiResults' => $allApiResults,
469 $this->_mut
->assertRecipients($this->getRecipients(1, $expectedTotal / $settings['mailings'], 'nul.example.com', $settings['mailings']));
470 $this->assertEquals(0, $apiCalls->getRunningCount());
474 * Create contacts in group.
477 * @param int $groupID
478 * @param string $domain
480 public function createContactsInGroup($count, $groupID, $domain = 'nul.example.com') {
481 for ($i = 1; $i <= $count; $i++
) {
482 $contactID = $this->individualCreate(['first_name' => $count, 'email' => 'mail' . $i . '@' . $domain]);
483 $this->callAPISuccess('group_contact', 'create', [
484 'contact_id' => $contactID,
485 'group_id' => $groupID,
492 * Construct the list of email addresses for $count recipients.
496 * @param string $domain
497 * @param int $mailings
501 public function getRecipients($start, $count, $domain = 'nul.example.com', $mailings = 1) {
503 for ($m = 0; $m < $mailings; $m++
) {
504 for ($i = $start; $i < ($start +
$count); $i++
) {
505 $recipients[][0] = 'mail' . $i . '@' . $domain;
511 protected function cleanupMailingTest() {
512 $this->quickCleanup([
514 'civicrm_mailing_job',
515 'civicrm_mailing_spool',
516 'civicrm_mailing_group',
517 'civicrm_mailing_recipients',
518 'civicrm_mailing_event_queue',
519 'civicrm_mailing_event_bounce',
520 'civicrm_mailing_event_delivered',
522 'civicrm_group_contact',
528 * Categorize results based on (a) whether they succeeded
529 * and (b) the number of messages sent.
531 * @param array $apiResults
533 * One key 'error' for all failures.
534 * A separate key for each distinct quantity.
536 protected function tallyApiResults($apiResults) {
538 foreach ($apiResults as $apiResult) {
539 $key = !empty($apiResult['is_error']) ?
'error' : $apiResult['values']['processed'];
540 $ret[$key] = !empty($ret[$key]) ?
1 +
$ret[$key] : 1;