Merge pull request #7797 from JKingsnorth/CRM-17977
[civicrm-core.git] / tests / phpunit / api / v3 / JobTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
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-2016
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 */
43 class api_v3_JobTest extends CiviUnitTestCase {
44 protected $_apiversion = 3;
45
46 public $DBResetRequired = FALSE;
47 public $_entity = 'Job';
48 public $_params = array();
49
50 public function setUp() {
51 parent::setUp();
52 $this->useTransaction(TRUE);
53 $this->_params = array(
54 'sequential' => 1,
55 'name' => 'API_Test_Job',
56 'description' => 'A long description written by hand in cursive',
57 'run_frequency' => 'Daily',
58 'api_entity' => 'ApiTestEntity',
59 'api_action' => 'apitestaction',
60 'parameters' => 'Semi-formal explanation of runtime job parameters',
61 'is_active' => 1,
62 );
63 }
64
65 /**
66 * Check with no name.
67 */
68 public function testCreateWithoutName() {
69 $params = array(
70 'is_active' => 1,
71 );
72 $this->callAPIFailure('job', 'create', $params,
73 'Mandatory key(s) missing from params array: run_frequency, name, api_entity, api_action'
74 );
75 }
76
77 /**
78 * Create job with an invalid "run_frequency" value.
79 */
80 public function testCreateWithInvalidFrequency() {
81 $params = array(
82 'sequential' => 1,
83 'name' => 'API_Test_Job',
84 'description' => 'A long description written by hand in cursive',
85 'run_frequency' => 'Fortnightly',
86 'api_entity' => 'ApiTestEntity',
87 'api_action' => 'apitestaction',
88 'parameters' => 'Semi-formal explanation of runtime job parameters',
89 'is_active' => 1,
90 );
91 $this->callAPIFailure('job', 'create', $params);
92 }
93
94 /**
95 * Create job.
96 */
97 public function testCreate() {
98 $result = $this->callAPIAndDocument('job', 'create', $this->_params, __FUNCTION__, __FILE__);
99 $this->assertNotNull($result['values'][0]['id']);
100
101 // mutate $params to match expected return value
102 unset($this->_params['sequential']);
103 //assertDBState compares expected values in $result to actual values in the DB
104 $this->assertDBState('CRM_Core_DAO_Job', $result['id'], $this->_params);
105 }
106
107 /**
108 * Check with empty array.
109 */
110 public function testDeleteEmpty() {
111 $params = array();
112 $result = $this->callAPIFailure('job', 'delete', $params);
113 }
114
115 /**
116 * Check with No array.
117 */
118 public function testDeleteParamsNotArray() {
119 $result = $this->callAPIFailure('job', 'delete', 'string');
120 }
121
122 /**
123 * Check if required fields are not passed.
124 */
125 public function testDeleteWithoutRequired() {
126 $params = array(
127 'name' => 'API_Test_PP',
128 'title' => 'API Test Payment Processor',
129 'class_name' => 'CRM_Core_Payment_APITest',
130 );
131
132 $result = $this->callAPIFailure('job', 'delete', $params);
133 $this->assertEquals($result['error_message'], 'Mandatory key(s) missing from params array: id');
134 }
135
136 /**
137 * Check with incorrect required fields.
138 */
139 public function testDeleteWithIncorrectData() {
140 $params = array(
141 'id' => 'abcd',
142 );
143 $result = $this->callAPIFailure('job', 'delete', $params);
144 }
145
146 /**
147 * Check job delete.
148 */
149 public function testDelete() {
150 $createResult = $this->callAPISuccess('job', 'create', $this->_params);
151 $params = array('id' => $createResult['id']);
152 $result = $this->callAPIAndDocument('job', 'delete', $params, __FUNCTION__, __FILE__);
153 $this->assertAPIDeleted($this->_entity, $createResult['id']);
154 }
155
156 /**
157 *
158 * public function testCallUpdateGreetingMissingParams() {
159 * $result = $this->callAPISuccess($this->_entity, 'update_greeting', array('gt' => 1));
160 * $this->assertEquals('Mandatory key(s) missing from params array: ct', $result['error_message']);
161 * }
162 *
163 * public function testCallUpdateGreetingIncorrectParams() {
164 * $result = $this->callAPISuccess($this->_entity, 'update_greeting', array('gt' => 1, 'ct' => 'djkfhdskjfhds'));
165 * $this->assertEquals('ct `djkfhdskjfhds` is not valid.', $result['error_message']);
166 * }
167 * /*
168 * Note that this test is about tesing the metadata / calling of the function & doesn't test the success of the called function
169 */
170 public function testCallUpdateGreetingSuccess() {
171 $result = $this->callAPISuccess($this->_entity, 'update_greeting', array(
172 'gt' => 'postal_greeting',
173 'ct' => 'Individual',
174 ));
175 }
176
177 public function testCallUpdateGreetingCommaSeparatedParamsSuccess() {
178 $gt = 'postal_greeting,email_greeting,addressee';
179 $ct = 'Individual,Household';
180 $result = $this->callAPISuccess($this->_entity, 'update_greeting', array('gt' => $gt, 'ct' => $ct));
181 }
182
183 /**
184 * Test the call reminder success sends more than 25 reminders & is not incorrectly limited.
185 *
186 * Note that this particular test sends the reminders to the additional recipients only
187 * as no real reminder person is configured
188 *
189 * Also note that this is testing a 'job' api so is in this class rather than scheduled_reminder - which
190 * seems a cleaner place to build up a collection of scheduled reminder testing functions. However, it seems
191 * that the api itself would need to be moved to the scheduled_reminder fn to do that with the job wrapper being respected for legacy functions
192 */
193 public function testCallSendReminderSuccessMoreThanDefaultLimit() {
194 $membershipTypeID = $this->membershipTypeCreate();
195 $this->membershipStatusCreate();
196 $createTotal = 30;
197 for ($i = 1; $i <= $createTotal; $i++) {
198 $contactID = $this->individualCreate();
199 $groupID = $this->groupCreate(array('name' => $i, 'title' => $i));
200 $result = $this->callAPISuccess('action_schedule', 'create', array(
201 'title' => " job $i",
202 'subject' => "job $i",
203 'entity_value' => $membershipTypeID,
204 'mapping_id' => 4,
205 'start_action_date' => 'membership_join_date',
206 'start_action_offset' => 0,
207 'start_action_condition' => 'before',
208 'start_action_unit' => 'hour',
209 'group_id' => $groupID,
210 'limit_to' => FALSE,
211 ));
212 $this->callAPISuccess('group_contact', 'create', array(
213 'contact_id' => $contactID,
214 'status' => 'Added',
215 'group_id' => $groupID,
216 ));
217 }
218 $result = $this->callAPISuccess('job', 'send_reminder', array());
219 $successfulCronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
220 $this->assertEquals($successfulCronCount, $createTotal);
221 }
222
223 /**
224 * Test scheduled reminders respect limit to (since above identified addition_to handling issue).
225 *
226 * We create 3 contacts - 1 is in our group, 1 has our membership & the chosen one has both
227 * & check that only the chosen one got the reminder
228 */
229 public function testCallSendReminderLimitTo() {
230 $membershipTypeID = $this->membershipTypeCreate();
231 $this->membershipStatusCreate();
232 $createTotal = 3;
233 $groupID = $this->groupCreate(array('name' => 'Texan drawlers', 'title' => 'a...'));
234 for ($i = 1; $i <= $createTotal; $i++) {
235 $contactID = $this->individualCreate();
236 if ($i == 2) {
237 $theChosenOneID = $contactID;
238 }
239 if ($i < 3) {
240 $this->callAPISuccess('group_contact', 'create', array(
241 'contact_id' => $contactID,
242 'status' => 'Added',
243 'group_id' => $groupID,
244 ));
245 }
246 if ($i > 1) {
247 $this->callAPISuccess('membership', 'create', array(
248 'contact_id' => $contactID,
249 'membership_type_id' => $membershipTypeID,
250 'join_date' => 'now',
251 'start_date' => '+ 1 hour',
252 )
253 );
254 }
255 }
256 $this->callAPISuccess('action_schedule', 'create', array(
257 'title' => " remind all Texans",
258 'subject' => "drawling renewal",
259 'entity_value' => $membershipTypeID,
260 'mapping_id' => 4,
261 'start_action_date' => 'membership_start_date',
262 'start_action_offset' => 0,
263 'start_action_condition' => 'before',
264 'start_action_unit' => 'hour',
265 'group_id' => $groupID,
266 'limit_to' => TRUE,
267 ));
268 $this->callAPISuccess('job', 'send_reminder', array());
269 $successfulCronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
270 $this->assertEquals($successfulCronCount, 1);
271 $sentToID = CRM_Core_DAO::singleValueQuery("SELECT contact_id FROM civicrm_action_log");
272 $this->assertEquals($sentToID, $theChosenOneID);
273 }
274
275 public function testCallDisableExpiredRelationships() {
276 $individualID = $this->individualCreate();
277 $orgID = $this->organizationCreate();
278 CRM_Utils_Hook_UnitTests::singleton()->setHook('civicrm_pre', array($this, 'hookPreRelationship'));
279 $relationshipTypeID = $this->callAPISuccess('relationship_type', 'getvalue', array(
280 'return' => 'id',
281 'name_a_b' => 'Employee of',
282 ));
283 $result = $this->callAPISuccess('relationship', 'create', array(
284 'relationship_type_id' => $relationshipTypeID,
285 'contact_id_a' => $individualID,
286 'contact_id_b' => $orgID,
287 'is_active' => 1,
288 'end_date' => 'yesterday',
289 ));
290 $relationshipID = $result['id'];
291 $this->assertEquals('Hooked', $result['values'][$relationshipID]['description']);
292 $this->callAPISuccess($this->_entity, 'disable_expired_relationships', array());
293 $result = $this->callAPISuccess('relationship', 'get', array());
294 $this->assertEquals('Go Go you good thing', $result['values'][$relationshipID]['description']);
295 $this->contactDelete($individualID);
296 $this->contactDelete($orgID);
297 }
298
299 /**
300 * Test the batch merge function.
301 *
302 * We are just checking it returns without error here.
303 */
304 public function testBatchMerge() {
305 $this->callAPISuccess('Job', 'process_batch_merge', array());
306 }
307
308 /**
309 * Test the batch merge function actually works!
310 *
311 * @dataProvider getMergeSets
312 *
313 * @param $dataSet
314 */
315 public function testBatchMergeWorks($dataSet) {
316 foreach ($dataSet['contacts'] as $params) {
317 $this->callAPISuccess('Contact', 'create', $params);
318 }
319
320 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => $dataSet['mode']));
321 $this->assertEquals($dataSet['skipped'], count($result['values']['skipped']), 'Failed to skip the right number:' . $dataSet['skipped']);
322 $this->assertEquals($dataSet['merged'], count($result['values']['merged']));
323 $result = $this->callAPISuccess('Contact', 'get', array('contact_sub_type' => 'Student', 'sequential' => 1));
324 $this->assertEquals(count($dataSet['expected']), $result['count']);
325 foreach ($dataSet['expected'] as $index => $contact) {
326 foreach ($contact as $key => $value) {
327 $this->assertEquals($value, $result['values'][$index][$key]);
328 }
329 }
330 }
331
332 /**
333 * Test the batch merge does not create duplicate emails.
334 *
335 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
336 */
337 public function testBatchMergeEmailHandling() {
338 for ($x = 0; $x <= 4; $x++) {
339 $id = $this->individualCreate(array('email' => 'batman@gotham.met'));
340 }
341 $result = $this->callAPISuccess('Job', 'process_batch_merge', array());
342 $this->assertEquals(4, count($result['values']['merged']));
343 $this->callAPISuccessGetCount('Contact', array('email' => 'batman@gotham.met'), 1);
344 $contacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
345 $deletedContacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 1));
346 $this->callAPISuccessGetCount('Email', array(
347 'email' => 'batman@gotham.met',
348 'contact_id' => array('IN' => array_keys($contacts['values'])),
349 ), 1);
350 $this->callAPISuccessGetCount('Email', array(
351 'email' => 'batman@gotham.met',
352 'contact_id' => array('IN' => array_keys($deletedContacts['values'])),
353 ), 4);
354 }
355
356 /**
357 * Test the batch merge does not create duplicate emails.
358 *
359 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
360 */
361 public function testBatchMergeMatchingAddress() {
362 for ($x = 0; $x <= 2; $x++) {
363 $this->individualCreate(array(
364 'api.address.create' => array(
365 'location_type_id' => 'Home',
366 'street_address' => 'Appt 115, The Batcave',
367 'city' => 'Gotham',
368 'postal_code' => 'Nananananana',
369 ),
370 ));
371 }
372 // Different location type, still merge, identical.
373 $this->individualCreate(array(
374 'api.address.create' => array(
375 'location_type_id' => 'Main',
376 'street_address' => 'Appt 115, The Batcave',
377 'city' => 'Gotham',
378 'postal_code' => 'Nananananana',
379 ),
380 ));
381
382 $this->individualCreate(array(
383 'api.address.create' => array(
384 'location_type_id' => 'Home',
385 'street_address' => 'Appt 115, The Batcave',
386 'city' => 'Gotham',
387 'postal_code' => 'Batman',
388 ),
389 ));
390
391 $result = $this->callAPISuccess('Job', 'process_batch_merge', array());
392 $this->assertEquals(3, count($result['values']['merged']));
393 $this->assertEquals(1, count($result['values']['skipped']));
394 $this->callAPISuccessGetCount('Contact', array('street_address' => 'Appt 115, The Batcave'), 2);
395 $contacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
396 $deletedContacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 1));
397 $this->callAPISuccessGetCount('Address', array(
398 'street_address' => 'Appt 115, The Batcave',
399 'contact_id' => array('IN' => array_keys($contacts['values'])),
400 ), 3);
401
402 $this->callAPISuccessGetCount('Address', array(
403 'street_address' => 'Appt 115, The Batcave',
404 'contact_id' => array('IN' => array_keys($deletedContacts['values'])),
405 ), 2);
406 }
407
408 /**
409 * Test the batch merge by id range.
410 *
411 * We have 2 sets of 5 matches & set the merge only to merge the lower set.
412 */
413 public function testBatchMergeIDRange() {
414 for ($x = 0; $x <= 4; $x++) {
415 $id = $this->individualCreate(array('email' => 'batman@gotham.met'));
416 }
417 for ($x = 0; $x <= 4; $x++) {
418 $this->individualCreate(array('email' => 'robin@gotham.met'));
419 }
420 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('criteria' => array('contact' => array('id' => array('<' => $id)))));
421 $this->assertEquals(4, count($result['values']['merged']));
422 $this->callAPISuccessGetCount('Contact', array('email' => 'batman@gotham.met'), 1);
423 $this->callAPISuccessGetCount('Contact', array('email' => 'robin@gotham.met'), 5);
424 $contacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
425 $deletedContacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
426 $this->callAPISuccessGetCount('Email', array(
427 'email' => 'batman@gotham.met',
428 'contact_id' => array('IN' => array_keys($contacts['values'])),
429 ), 1);
430 $this->callAPISuccessGetCount('Email', array(
431 'email' => 'batman@gotham.met',
432 'contact_id' => array('IN' => array_keys($deletedContacts['values'])),
433 ), 1);
434 $this->callAPISuccessGetCount('Email', array(
435 'email' => 'robin@gotham.met',
436 'contact_id' => array('IN' => array_keys($contacts['values'])),
437 ), 5);
438
439 }
440
441 /**
442 * Get data for batch merge.
443 */
444 public function getMergeSets() {
445 $data = array(
446 array(
447 array(
448 'mode' => 'safe',
449 'contacts' => array(
450 array(
451 'first_name' => 'Michael',
452 'last_name' => 'Jackson',
453 'email' => 'michael@neverland.com',
454 'contact_type' => 'Individual',
455 'contact_sub_type' => 'Student',
456 'api.Address.create' => array(
457 'street_address' => 'big house',
458 'location_type_id' => 'Home',
459 ),
460 ),
461 array(
462 'first_name' => 'Michael',
463 'last_name' => 'Jackson',
464 'email' => 'michael@neverland.com',
465 'contact_type' => 'Individual',
466 'contact_sub_type' => 'Student',
467 ),
468 ),
469 'skipped' => 0,
470 'merged' => 1,
471 'expected' => array(
472 array(
473 'first_name' => 'Michael',
474 'last_name' => 'Jackson',
475 'email' => 'michael@neverland.com',
476 'contact_type' => 'Individual',
477 ),
478 ),
479 ),
480 ),
481 array(
482 array(
483 'mode' => 'safe',
484 'contacts' => array(
485 array(
486 'first_name' => 'Michael',
487 'last_name' => 'Jackson',
488 'email' => 'michael@neverland.com',
489 'contact_type' => 'Individual',
490 'contact_sub_type' => 'Student',
491 'api.Address.create' => array(
492 'street_address' => 'big house',
493 'location_type_id' => 'Home',
494 ),
495 ),
496 array(
497 'first_name' => 'Michael',
498 'last_name' => 'Jackson',
499 'email' => 'michael@neverland.com',
500 'contact_type' => 'Individual',
501 'contact_sub_type' => 'Student',
502 'api.Address.create' => array(
503 'street_address' => 'bigger house',
504 'location_type_id' => 'Home',
505 ),
506 ),
507 ),
508 'skipped' => 1,
509 'merged' => 0,
510 'expected' => array(
511 array(
512 'first_name' => 'Michael',
513 'last_name' => 'Jackson',
514 'email' => 'michael@neverland.com',
515 'contact_type' => 'Individual',
516 'street_address' => 'big house',
517 ),
518 array(
519 'first_name' => 'Michael',
520 'last_name' => 'Jackson',
521 'email' => 'michael@neverland.com',
522 'contact_type' => 'Individual',
523 'street_address' => 'bigger house',
524 ),
525 ),
526 ),
527 ),
528 array(
529 array(
530 'mode' => 'aggressive',
531 'contacts' => array(
532 array(
533 'first_name' => 'Michael',
534 'last_name' => 'Jackson',
535 'email' => 'michael@neverland.com',
536 'contact_type' => 'Individual',
537 'contact_sub_type' => 'Student',
538 'api.Address.create' => array(
539 'street_address' => 'big house',
540 'location_type_id' => 'Home',
541 ),
542 ),
543 array(
544 'first_name' => 'Michael',
545 'last_name' => 'Jackson',
546 'email' => 'michael@neverland.com',
547 'contact_type' => 'Individual',
548 'contact_sub_type' => 'Student',
549 'api.Address.create' => array(
550 'street_address' => 'bigger house',
551 'location_type_id' => 'Home',
552 ),
553 ),
554 ),
555 'skipped' => 0,
556 'merged' => 1,
557 'expected' => array(
558 array(
559 'first_name' => 'Michael',
560 'last_name' => 'Jackson',
561 'email' => 'michael@neverland.com',
562 'contact_type' => 'Individual',
563 'street_address' => 'big house',
564 ),
565 ),
566 ),
567 ),
568 );
569 return $data;
570 }
571
572 /**
573 * @param $op
574 * @param string $objectName
575 * @param int $id
576 * @param array $params
577 */
578 public function hookPreRelationship($op, $objectName, $id, &$params) {
579 if ($op == 'delete') {
580 return;
581 }
582 if ($params['is_active']) {
583 $params['description'] = 'Hooked';
584 }
585 else {
586 $params['description'] = 'Go Go you good thing';
587 }
588 }
589
590 }