Merge pull request #11193 from MiyaNoctem/CRM-21328-warning-on-empty-visibility
[civicrm-core.git] / tests / phpunit / api / v3 / JobProcessMailingTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
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-2017
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 = array();
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 CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW
64 $this->_groupID = $this->groupCreate();
65 $this->_email = 'test@test.test';
66 $this->_params = array(
67 'subject' => 'Accidents in cars cause children',
68 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.',
69 'name' => 'mailing name',
70 'created_id' => 1,
71 'groups' => array('include' => array($this->_groupID)),
72 'scheduled_date' => 'now',
73 );
74 $this->defaultSettings = array(
75 'mailings' => 1, // int, #mailings to send
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 );
85 $this->_mut = new CiviMailUtils($this, TRUE);
86 $this->callAPISuccess('mail_settings', 'get', array('api.mail_settings.create' => array('domain' => 'chaos.org')));
87 }
88
89 /**
90 */
91 public function tearDown() {
92 //$this->_mut->clearMessages();
93 $this->_mut->stop();
94 CRM_Utils_Hook::singleton()->reset();
95 CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; // DGW
96 //$this->cleanupMailingTest();
97 parent::tearDown();
98 }
99
100 public function testBasic() {
101 $this->createContactsInGroup(10, $this->_groupID);
102 Civi::settings()->add(array(
103 'mailerBatchLimit' => 2,
104 ));
105 $this->callAPISuccess('mailing', 'create', $this->_params);
106 $this->_mut->assertRecipients(array());
107 $this->callAPISuccess('job', 'process_mailing', array());
108 $this->_mut->assertRecipients($this->getRecipients(1, 2));
109 }
110
111 /**
112 * Test mail when in non-production environment.
113 *
114 */
115 public function testMailNonProductionRun() {
116 // Test in non-production mode.
117 $params = array(
118 'environment' => 'Staging',
119 );
120 $this->callAPISuccess('Setting', 'create', $params);
121 $this->createContactsInGroup(10, $this->_groupID);
122 Civi::settings()->add(array(
123 'mailerBatchLimit' => 2,
124 ));
125 $this->callAPISuccess('mailing', 'create', $this->_params);
126 $this->_mut->assertRecipients(array());
127 $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.");
128
129 // Test with runInNonProductionEnvironment param.
130 $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
131 $this->_mut->assertRecipients($this->getRecipients(1, 2));
132
133 // Test in production mode.
134 $params = array(
135 'environment' => 'Production',
136 );
137 $this->callAPISuccess('Setting', 'create', $params);
138 $this->callAPISuccess('job', 'process_mailing', array());
139 $this->_mut->assertRecipients($this->getRecipients(1, 2));
140 }
141
142 public function concurrencyExamples() {
143 $es = array();
144
145 // Launch 3 workers, but mailerJobsMax limits us to 1 worker.
146 $es[0] = array(
147 array(
148 'recipients' => 20,
149 'workers' => 3,
150 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
151 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
152 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
153 // trampling effects.
154 'lockHold' => 10,
155 'mailerBatchLimit' => 4,
156 'mailerJobsMax' => 1,
157 ),
158 array(
159 0 => 2, // 2 jobs which produce 0 messages
160 4 => 1, // 1 job which produces 4 messages
161 ),
162 4,
163 );
164
165 // Launch 3 workers, but mailerJobsMax limits us to 2 workers.
166 $es[1] = array(
167 array(// Settings.
168 'recipients' => 20,
169 'workers' => 3,
170 // FIXME: lockHold is unrealistic/unrepresentative. In reality, this situation fails because
171 // the data.* locks trample the worker.* locks. However, setting lockHold allows us to
172 // approximate the behavior of what would happen *if* the lock-implementation didn't suffer
173 // trampling effects.
174 'lockHold' => 10,
175 'mailerBatchLimit' => 5,
176 'mailerJobsMax' => 2,
177 ),
178 array(// Tallies.
179 0 => 1, // 1 job which produce 0 messages
180 5 => 2, // 2 jobs which produce 5 messages
181 ),
182 10, // Total sent.
183 );
184
185 // Launch 3 workers and saturate them (mailerJobsMax=3)
186 $es[2] = array(
187 array(// Settings.
188 'recipients' => 20,
189 'workers' => 3,
190 'mailerBatchLimit' => 6,
191 'mailerJobsMax' => 3,
192 ),
193 array(// Tallies.
194 6 => 3, // 3 jobs which produce 6 messages
195 ),
196 18, // Total sent.
197 );
198
199 // Launch 4 workers and saturate them (mailerJobsMax=0)
200 $es[3] = array(
201 array(// Settings.
202 'recipients' => 20,
203 'workers' => 4,
204 'mailerBatchLimit' => 6,
205 'mailerJobsMax' => 0,
206 ),
207 array(// Tallies.
208 6 => 3, // 3 jobs which produce 6 messages
209 2 => 1, // 1 job which produces 2 messages
210 ),
211 20, // Total sent.
212 );
213
214 // Launch 1 worker, 3 times in a row. Deliver everything.
215 $es[4] = array(
216 array(// Settings.
217 'recipients' => 10,
218 'workers' => 1,
219 'iterations' => 3,
220 'mailerBatchLimit' => 7,
221 ),
222 array(// Tallies.
223 7 => 1, // 1 job which produces 7 messages
224 3 => 1, // 1 job which produces 3 messages
225 0 => 1, // 1 job which produces 0 messages
226 ),
227 10, // Total sent.
228 );
229
230 // Launch 2 worker, 3 times in a row. Deliver everything.
231 $es[5] = array(
232 array(// Settings.
233 'recipients' => 10,
234 'workers' => 2,
235 'iterations' => 3,
236 'mailerBatchLimit' => 3,
237 ),
238 array(// Tallies.
239 3 => 3, // 3 jobs which produce 3 messages
240 1 => 1, // 1 job which produces 1 messages
241 0 => 2, // 2 jobs which produce 0 messages
242 ),
243 10, // Total sent.
244 );
245
246 // For two mailings, launch 1 worker, 5 times in a row. Deliver everything.
247 $es[6] = array(
248 array(// Settings.
249 'mailings' => 2,
250 'recipients' => 10,
251 'workers' => 1,
252 'iterations' => 5,
253 'mailerBatchLimit' => 6,
254 ),
255 array(// Tallies.
256 // x6 => x4+x2 => x6 => x2 => x0
257 6 => 3, // 3 jobs which produce 6 messages
258 2 => 1, // 1 job which produces 2 messages
259 0 => 1, // 1 job which produces 0 messages
260 ),
261 20, // Total sent.
262 );
263
264 return $es;
265 }
266
267 /**
268 * Setup various mail configuration options (eg $mailerBatchLimit,
269 * $mailerJobMax) and spawn multiple worker threads ($workers).
270 * Allow the threads to complete. (Optionally, repeat the above
271 * process.) Finally, check to see if the right number of
272 * jobs delivered the right number of messages.
273 *
274 * @param array $settings
275 * An array of settings (eg mailerBatchLimit, workers). See comments
276 * for $this->defaultSettings.
277 * @param array $expectedTallies
278 * A listing of the number cron-runs keyed by their size.
279 * For example, array(10=>2) means that there 2 cron-runs
280 * which delivered 10 messages each.
281 * @param int $expectedTotal
282 * The total number of contacts for whom messages should have
283 * been sent.
284 * @dataProvider concurrencyExamples
285 */
286 public function testConcurrency($settings, $expectedTallies, $expectedTotal) {
287 $settings = array_merge($this->defaultSettings, $settings);
288
289 $this->createContactsInGroup($settings['recipients'], $this->_groupID);
290 Civi::settings()->add(CRM_Utils_Array::subset($settings, array(
291 'mailerBatchLimit',
292 'mailerJobsMax',
293 'mailThrottleTime',
294 )));
295
296 for ($i = 0; $i < $settings['mailings']; $i++) {
297 $this->callAPISuccess('mailing', 'create', $this->_params);
298 }
299
300 $this->_mut->assertRecipients(array());
301
302 $allApiResults = array();
303 for ($iterationId = 0; $iterationId < $settings['iterations']; $iterationId++) {
304 $apiCalls = $this->createExternalAPI();
305 $apiCalls->addEnv(array('CIVICRM_CRON_HOLD' => $settings['lockHold']));
306 for ($workerId = 0; $workerId < $settings['workers']; $workerId++) {
307 $apiCalls->addCall('job', 'process_mailing', array());
308 }
309 $apiCalls->start();
310 $this->assertEquals($settings['workers'], $apiCalls->getRunningCount());
311
312 $apiCalls->wait();
313 $allApiResults = array_merge($allApiResults, $apiCalls->getResults());
314 }
315
316 $actualTallies = $this->tallyApiResults($allApiResults);
317 $this->assertEquals($expectedTallies, $actualTallies, 'API tallies should match.' . print_r(array(
318 'expectedTallies' => $expectedTallies,
319 'actualTallies' => $actualTallies,
320 'apiResults' => $allApiResults,
321 ), TRUE));
322 $this->_mut->assertRecipients($this->getRecipients(1, $expectedTotal / $settings['mailings'], 'nul.example.com', $settings['mailings']));
323 $this->assertEquals(0, $apiCalls->getRunningCount());
324 }
325
326 /**
327 * Create contacts in group.
328 *
329 * @param int $count
330 * @param int $groupID
331 * @param string $domain
332 */
333 public function createContactsInGroup($count, $groupID, $domain = 'nul.example.com') {
334 for ($i = 1; $i <= $count; $i++) {
335 $contactID = $this->individualCreate(array('first_name' => $count, 'email' => 'mail' . $i . '@' . $domain));
336 $this->callAPISuccess('group_contact', 'create', array(
337 'contact_id' => $contactID,
338 'group_id' => $groupID,
339 'status' => 'Added',
340 ));
341 }
342 }
343
344 /**
345 * Construct the list of email addresses for $count recipients.
346 *
347 * @param int $start
348 * @param int $count
349 * @param string $domain
350 * @param int $mailings
351 *
352 * @return array
353 */
354 public function getRecipients($start, $count, $domain = 'nul.example.com', $mailings = 1) {
355 $recipients = array();
356 for ($m = 0; $m < $mailings; $m++) {
357 for ($i = $start; $i < ($start + $count); $i++) {
358 $recipients[][0] = 'mail' . $i . '@' . $domain;
359 }
360 }
361 return $recipients;
362 }
363
364 protected function cleanupMailingTest() {
365 $this->quickCleanup(array(
366 'civicrm_mailing',
367 'civicrm_mailing_job',
368 'civicrm_mailing_spool',
369 'civicrm_mailing_group',
370 'civicrm_mailing_recipients',
371 'civicrm_mailing_event_queue',
372 'civicrm_mailing_event_bounce',
373 'civicrm_mailing_event_delivered',
374 'civicrm_group',
375 'civicrm_group_contact',
376 'civicrm_contact',
377 ));
378 }
379
380 /**
381 * Categorize results based on (a) whether they succeeded
382 * and (b) the number of messages sent.
383 *
384 * @param array $apiResults
385 * @return array
386 * One key 'error' for all failures.
387 * A separate key for each distinct quantity.
388 */
389 protected function tallyApiResults($apiResults) {
390 $ret = array();
391 foreach ($apiResults as $apiResult) {
392 $key = !empty($apiResult['is_error']) ? 'error' : $apiResult['values']['processed'];
393 $ret[$key] = !empty($ret[$key]) ? 1 + $ret[$key] : 1;
394 }
395 return $ret;
396 }
397
398 }