Merge pull request #19943 from civicrm/5.36
[civicrm-core.git] / tests / phpunit / CRM / Case / XMLProcessor / ProcessTest.php
1 <?php
2 require_once 'CiviTest/CiviCaseTestCase.php';
3
4 /**
5 * Class CRM_Case_PseudoConstantTest
6 * @group headless
7 */
8 class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
9
10 public function setUp(): void {
11 parent::setUp();
12
13 $this->defaultAssigneeOptionsValues = [];
14
15 $this->setupContacts();
16 $this->setupDefaultAssigneeOptions();
17 $this->setupRelationships();
18 $this->setupMoreRelationshipTypes();
19 $this->setupActivityDefinitions();
20
21 $this->process = new CRM_Case_XMLProcessor_Process();
22 }
23
24 public function tearDown(): void {
25 $this->deleteMoreRelationshipTypes();
26
27 parent::tearDown();
28 }
29
30 /**
31 * Creates sample contacts.
32 */
33 protected function setUpContacts() {
34 $this->contacts = [
35 'ana' => $this->individualCreate(),
36 'beto' => $this->individualCreate(),
37 'carlos' => $this->individualCreate(),
38 ];
39 }
40
41 /**
42 * Adds the default assignee group and options to the test database.
43 * It also stores the IDs of the options in an index.
44 */
45 protected function setupDefaultAssigneeOptions() {
46 $options = [
47 'NONE', 'BY_RELATIONSHIP', 'SPECIFIC_CONTACT', 'USER_CREATING_THE_CASE',
48 ];
49
50 CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
51 'name' => 'activity_default_assignee',
52 ]);
53
54 foreach ($options as $option) {
55 $optionValue = CRM_Core_BAO_OptionValue::ensureOptionValueExists([
56 'option_group_id' => 'activity_default_assignee',
57 'name' => $option,
58 'label' => $option,
59 ]);
60
61 $this->defaultAssigneeOptionsValues[$option] = $optionValue['value'];
62 }
63 }
64
65 /**
66 * Adds a relationship between the activity's target contact and default assignee.
67 */
68 protected function setupRelationships() {
69 $this->relationships = [
70 'ana_is_pupil_of_beto' => [
71 'type_id' => NULL,
72 'name_a_b' => 'Pupil of',
73 'name_b_a' => 'Instructor',
74 'contact_id_a' => $this->contacts['ana'],
75 'contact_id_b' => $this->contacts['beto'],
76 ],
77 'ana_is_spouse_of_carlos' => [
78 'type_id' => NULL,
79 'name_a_b' => 'Spouse of',
80 'name_b_a' => 'Spouse of',
81 'contact_id_a' => $this->contacts['ana'],
82 'contact_id_b' => $this->contacts['carlos'],
83 ],
84 'unassigned_employee' => [
85 'type_id' => NULL,
86 'name_a_b' => 'Employee of',
87 'name_b_a' => 'Employer',
88 ],
89 ];
90
91 foreach ($this->relationships as $name => &$relationship) {
92 $relationship['type_id'] = $this->relationshipTypeCreate([
93 'contact_type_a' => 'Individual',
94 'contact_type_b' => 'Individual',
95 'name_a_b' => $relationship['name_a_b'],
96 'label_a_b' => $relationship['name_a_b'],
97 'name_b_a' => $relationship['name_b_a'],
98 'label_b_a' => $relationship['name_b_a'],
99 ]);
100
101 if (isset($relationship['contact_id_a'])) {
102 $this->callAPISuccess('Relationship', 'create', [
103 'contact_id_a' => $relationship['contact_id_a'],
104 'contact_id_b' => $relationship['contact_id_b'],
105 'relationship_type_id' => $relationship['type_id'],
106 ]);
107 }
108 }
109 }
110
111 /**
112 * Set up some additional relationship types for some specific tests.
113 */
114 protected function setupMoreRelationshipTypes() {
115 $this->moreRelationshipTypes = [
116 'unidirectional_name_label_different' => [
117 'type_id' => NULL,
118 'name_a_b' => 'jm7ab',
119 'label_a_b' => 'Jedi Master is',
120 'name_b_a' => 'jm7ba',
121 'label_b_a' => 'Jedi Master for',
122 'description' => 'Jedi Master',
123 ],
124 'unidirectional_name_label_same' => [
125 'type_id' => NULL,
126 'name_a_b' => 'Quilt Maker is',
127 'label_a_b' => 'Quilt Maker is',
128 'name_b_a' => 'Quilt Maker for',
129 'label_b_a' => 'Quilt Maker for',
130 'description' => 'Quilt Maker',
131 ],
132 'bidirectional_name_label_different' => [
133 'type_id' => NULL,
134 'name_a_b' => 'f12',
135 'label_a_b' => 'Friend of',
136 'name_b_a' => 'f12',
137 'label_b_a' => 'Friend of',
138 'description' => 'Friend',
139 ],
140 'bidirectional_name_label_same' => [
141 'type_id' => NULL,
142 'name_a_b' => 'Enemy of',
143 'label_a_b' => 'Enemy of',
144 'name_b_a' => 'Enemy of',
145 'label_b_a' => 'Enemy of',
146 'description' => 'Enemy',
147 ],
148 ];
149
150 foreach ($this->moreRelationshipTypes as &$relationship) {
151 $relationship['type_id'] = $this->relationshipTypeCreate([
152 'contact_type_a' => 'Individual',
153 'contact_type_b' => 'Individual',
154 'name_a_b' => $relationship['name_a_b'],
155 'label_a_b' => $relationship['label_a_b'],
156 'name_b_a' => $relationship['name_b_a'],
157 'label_b_a' => $relationship['label_b_a'],
158 'description' => $relationship['description'],
159 ]);
160 }
161 }
162
163 /**
164 * Clean up additional relationship types (tearDown).
165 */
166 protected function deleteMoreRelationshipTypes() {
167 foreach ($this->moreRelationshipTypes as $relationship) {
168 $this->callAPISuccess('relationship_type', 'delete', ['id' => $relationship['type_id']]);
169 }
170 }
171
172 /**
173 * Defines the the activity parameters and XML definitions. These can be used
174 * to create the activity.
175 */
176 protected function setupActivityDefinitions() {
177 $activityTypeXml = '<activity-type><name>Open Case</name></activity-type>';
178 $this->activityTypeXml = new SimpleXMLElement($activityTypeXml);
179 $this->activityParams = [
180 'activity_date_time' => date('Ymd'),
181 // @todo This seems wrong, it just happens to work out because both caseId and caseTypeId equal 1 in the stock setup here.
182 'caseID' => $this->caseTypeId,
183 'clientID' => $this->contacts['ana'],
184 'creatorID' => $this->_loggedInUser,
185 ];
186 }
187
188 /**
189 * Tests the creation of activities where the default assignee should be the
190 * target contact's instructor. Beto is the instructor for Ana.
191 */
192 public function testCreateActivityWithDefaultContactByRelationship() {
193 $relationship = $this->relationships['ana_is_pupil_of_beto'];
194 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
195 $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_b_a";
196
197 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
198 $this->assertActivityAssignedToContactExists($this->contacts['beto']);
199 }
200
201 /**
202 * Test the creation of activities where the default assignee should not
203 * end up being a contact from another case where it has the same client
204 * and relationship.
205 */
206 public function testCreateActivityWithDefaultContactByRelationshipTwoCases() {
207 /*
208 At this point the stock setup looks like this:
209 Case 1: no roles assigned
210 Non-case relationship with ana as pupil of beto
211 Non-case relationship with ana as spouse of carlos
212
213 So we want to:
214 Make another case for the same client ana.
215 Add a pupil role on that new case with some other person.
216 Make an activity on the first case.
217
218 Since there is a non-case relationship of that type for the
219 right person we do want it to take that one even though there is no role
220 on the first case, i.e. it SHOULD fall back to non-case relationships.
221 So this is test 1.
222
223 Then we want to get rid of the non-case relationship and try again. In
224 this situation it should not make any assignment, i.e. it should not
225 take the other person from the other case. The original bug was that it
226 would assign the activity to that other person from the other case. This
227 is test 2.
228 */
229
230 $relationship = $this->relationships['ana_is_pupil_of_beto'];
231
232 // Make another case and add a case role with the same relationship we
233 // want, but a different person.
234 $caseObj = $this->createCase($this->contacts['ana'], $this->_loggedInUser);
235 $this->callAPISuccess('Relationship', 'create', [
236 'contact_id_a' => $this->contacts['ana'],
237 'contact_id_b' => $this->contacts['carlos'],
238 'relationship_type_id' => $relationship['type_id'],
239 'case_id' => $caseObj->id,
240 ]);
241
242 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
243 $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_b_a";
244
245 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
246
247 // We can't use assertActivityAssignedToContactExists because it assumes
248 // there's only one activity in the database, but we have several from the
249 // second case. We want the one we just created on the first case.
250 $result = $this->callAPISuccess('Activity', 'get', [
251 'case_id' => $this->activityParams['caseID'],
252 'return' => ['assignee_contact_id'],
253 ])['values'];
254 $this->assertCount(1, $result);
255 foreach ($result as $activity) {
256 // Note the first parameter is turned into an array to match the second.
257 $this->assertEquals([$this->contacts['beto']], $activity['assignee_contact_id']);
258 }
259
260 // Now remove the non-case relationship.
261 $result = $this->callAPISuccess('Relationship', 'get', [
262 'case_id' => ['IS NULL' => 1],
263 'relationship_type_id' => $relationship['type_id'],
264 'contact_id_a' => $this->contacts['ana'],
265 'contact_id_b' => $this->contacts['beto'],
266 ])['values'];
267 $this->assertCount(1, $result);
268 foreach ($result as $activity) {
269 $result = $this->callAPISuccess('Relationship', 'delete', ['id' => $activity['id']]);
270 }
271
272 // Create another activity on the first case. Make it a different activity
273 // type so we can find it better.
274 $activityXml = '<activity-type><name>Follow up</name></activity-type>';
275 $activityXmlElement = new SimpleXMLElement($activityXml);
276 $activityXmlElement->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
277 $activityXmlElement->default_assignee_relationship = "{$relationship['type_id']}_b_a";
278 $this->process->createActivity($activityXmlElement, $this->activityParams);
279
280 $result = $this->callAPISuccess('Activity', 'get', [
281 'case_id' => $this->activityParams['caseID'],
282 'activity_type_id' => 'Follow up',
283 'return' => ['assignee_contact_id'],
284 ])['values'];
285 $this->assertCount(1, $result);
286 foreach ($result as $activity) {
287 // It should be empty, not the contact from the second case.
288 $this->assertEmpty($activity['assignee_contact_id']);
289 }
290 }
291
292 /**
293 * Tests when the default assignee relationship exists, but in the other direction only.
294 * Ana is a pupil, but has no pupils related to her.
295 */
296 public function testCreateActivityWithDefaultContactByRelationshipMissing() {
297 $relationship = $this->relationships['ana_is_pupil_of_beto'];
298 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
299 $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_a_b";
300
301 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
302 $this->assertActivityAssignedToContactExists(NULL);
303 }
304
305 /**
306 * Tests when the the default assignee relationship exists and is a bidirectional
307 * relationship. Ana and Carlos are spouses.
308 */
309 public function testCreateActivityWithDefaultContactByRelationshipBidirectional() {
310 $relationship = $this->relationships['ana_is_spouse_of_carlos'];
311 $this->activityParams['clientID'] = $this->contacts['carlos'];
312 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
313 $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_a_b";
314
315 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
316 $this->assertActivityAssignedToContactExists($this->contacts['ana']);
317 }
318
319 /**
320 * Tests when the default assignee relationship does not exist. Ana is not an
321 * employee for anyone.
322 */
323 public function testCreateActivityWithDefaultContactByRelationButTheresNoRelationship() {
324 $relationship = $this->relationships['unassigned_employee'];
325 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
326 $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_b_a";
327
328 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
329 $this->assertActivityAssignedToContactExists(NULL);
330 }
331
332 /**
333 * Tests the creation of activities with default assignee set to a specific contact.
334 */
335 public function testCreateActivityAssignedToSpecificContact() {
336 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['SPECIFIC_CONTACT'];
337 $this->activityTypeXml->default_assignee_contact = $this->contacts['carlos'];
338
339 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
340 $this->assertActivityAssignedToContactExists($this->contacts['carlos']);
341 }
342
343 /**
344 * Tests the creation of activities with default assignee set to a specific contact,
345 * but the contact does not exist.
346 */
347 public function testCreateActivityAssignedToNonExistantSpecificContact() {
348 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['SPECIFIC_CONTACT'];
349 $this->activityTypeXml->default_assignee_contact = 987456321;
350
351 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
352 $this->assertActivityAssignedToContactExists(NULL);
353 }
354
355 /**
356 * Tests the creation of activities with the default assignee being the one
357 * creating the case's activity.
358 */
359 public function testCreateActivityAssignedToUserCreatingTheCase() {
360 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['USER_CREATING_THE_CASE'];
361
362 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
363 $this->assertActivityAssignedToContactExists($this->_loggedInUser);
364 }
365
366 /**
367 * Tests the creation of activities when the default assignee is set to NONE.
368 */
369 public function testCreateActivityAssignedNoUser() {
370 $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['NONE'];
371
372 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
373 $this->assertActivityAssignedToContactExists(NULL);
374 }
375
376 /**
377 * Tests the creation of activities when the default assignee is set to NONE.
378 */
379 public function testCreateActivityWithNoDefaultAssigneeOption() {
380 $this->process->createActivity($this->activityTypeXml, $this->activityParams);
381 $this->assertActivityAssignedToContactExists(NULL);
382 }
383
384 /**
385 * Asserts that an activity was created where the assignee was the one related
386 * to the target contact.
387 *
388 * @param int|null $assigneeContactId the ID of the expected assigned contact or NULL if expected to be empty.
389 */
390 protected function assertActivityAssignedToContactExists($assigneeContactId) {
391 $expectedContact = $assigneeContactId === NULL ? [] : [$assigneeContactId];
392 $result = $this->callAPISuccess('Activity', 'get', [
393 'target_contact_id' => $this->activityParams['clientID'],
394 'return' => ['assignee_contact_id'],
395 ]);
396 $activity = CRM_Utils_Array::first($result['values']);
397
398 $this->assertNotNull($activity, 'Target contact has no activities assigned to them');
399 $this->assertEquals($expectedContact, $activity['assignee_contact_id'], 'Activity is not assigned to expected contact');
400 }
401
402 /**
403 * Test that caseRoles() doesn't have name and label mixed up.
404 *
405 * @param $key string The array key in the moreRelationshipTypes array that
406 * is the relationship type we're currently testing. So not necessarily
407 * unique for each entry in the dataprovider since want to test a given
408 * relationship type against multiple xml strings. It's not a test
409 * identifier, it's an array key to use to look up something.
410 * @param $xmlString string
411 * @param $expected array
412 * @param $dontcare array We're re-using the data provider for two tests and
413 * we don't care about those expected values.
414 *
415 * @dataProvider xmlCaseRoleDataProvider
416 */
417 public function testCaseRoles($key, $xmlString, $expected, $dontcare) {
418 $xmlObj = new SimpleXMLElement($xmlString);
419
420 // element 0 is direction (a_b), 1 is the text we want
421 $expectedArray = empty($expected) ? [] : ["{$this->moreRelationshipTypes[$key]['type_id']}_{$expected[0]}" => $expected[1]];
422
423 $this->assertEquals($expectedArray, $this->process->caseRoles($xmlObj->CaseRoles, FALSE));
424 }
425
426 /**
427 * Test that locateNameOrLabel doesn't have name and label mixed up.
428 *
429 * @param $key string The array key in the moreRelationshipTypes array that
430 * is the relationship type we're currently testing. So not necessarily
431 * unique for each entry in the dataprovider since want to test a given
432 * relationship type against multiple xml strings. It's not a test
433 * identifier, it's an array key to use to look up something.
434 * @param $xmlString string
435 * @param $dontcare array We're re-using the data provider for two tests and
436 * we don't care about those expected values.
437 * @param $expected array
438 *
439 * @dataProvider xmlCaseRoleDataProvider
440 */
441 public function testLocateNameOrLabel($key, $xmlString, $dontcare, $expected) {
442 $xmlObj = new SimpleXMLElement($xmlString);
443
444 // element 0 is direction (a_b), 1 is the text we want.
445 // In case of failure, the function is expected to return FALSE for the
446 // direction and then for the text it just gives us back the string we
447 // gave it.
448 $expectedArray = empty($expected[0])
449 ? [FALSE, $expected[1]]
450 : ["{$this->moreRelationshipTypes[$key]['type_id']}_{$expected[0]}", $expected[1]];
451
452 $this->assertEquals($expectedArray, $this->process->locateNameOrLabel($xmlObj->CaseRoles->RelationshipType));
453 }
454
455 /**
456 * Data provider for testCaseRoles and testLocateNameOrLabel
457 * @return array
458 */
459 public function xmlCaseRoleDataProvider() {
460 return [
461 // Simulate one that has been converted to the format it should be going
462 // forward, where name is the actual name, i.e. same as machineName.
463 [
464 // this is the array key in the $this->moreRelationshipTypes array
465 'unidirectional_name_label_different',
466 // some xml
467 '<CaseType><CaseRoles><RelationshipType><name>jm7ba</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
468 // this is the expected for testCaseRoles
469 ['a_b', 'Jedi Master is'],
470 // this is the expected for testLocateNameOrLabel
471 ['a_b', 'jm7ba'],
472 ],
473 // Simulate one that is still in label format, i.e. one that is still in
474 // xml files that haven't been updated, or in the db but upgrade script
475 // not run yet.
476 [
477 'unidirectional_name_label_different',
478 '<CaseType><CaseRoles><RelationshipType><name>Jedi Master for</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
479 ['a_b', 'Jedi Master is'],
480 ['a_b', 'jm7ba'],
481 ],
482 // Ditto but where we know name and label are the same in the db.
483 [
484 'unidirectional_name_label_same',
485 '<CaseType><CaseRoles><RelationshipType><name>Quilt Maker for</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
486 ['a_b', 'Quilt Maker is'],
487 ['a_b', 'Quilt Maker for'],
488 ],
489 // Simulate one that is messed up and should fail, e.g. like a typo
490 // in an xml file. Here we've made a typo on purpose.
491 [
492 'unidirectional_name_label_different',
493 '<CaseType><CaseRoles><RelationshipType><name>Jedi Masterrrr for</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
494 NULL,
495 [FALSE, 'Jedi Masterrrr for'],
496 ],
497 // Now some similar tests to above but for bidirectional relationships.
498 // Bidirectional relationship, name and label different, using machine name.
499 [
500 'bidirectional_name_label_different',
501 '<CaseType><CaseRoles><RelationshipType><name>f12</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
502 ['b_a', 'Friend of'],
503 ['b_a', 'f12'],
504 ],
505 // Bidirectional relationship, name and label different, using display label.
506 [
507 'bidirectional_name_label_different',
508 '<CaseType><CaseRoles><RelationshipType><name>Friend of</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
509 ['b_a', 'Friend of'],
510 ['b_a', 'f12'],
511 ],
512 // Bidirectional relationship, name and label same.
513 [
514 'bidirectional_name_label_same',
515 '<CaseType><CaseRoles><RelationshipType><name>Enemy of</name><creator>1</creator><manager>1</manager></RelationshipType></CaseRoles></CaseType>',
516 ['b_a', 'Enemy of'],
517 ['b_a', 'Enemy of'],
518 ],
519 ];
520 }
521
522 /**
523 * Test XMLProcessor activityTypes()
524 */
525 public function testXmlProcessorActivityTypes() {
526 // First change an activity's label since we also test getting the labels.
527 // @todo Having a brain freeze or something - can't do this in one step?
528 $activity_type_id = $this->callApiSuccess('OptionValue', 'get', [
529 'option_group_id' => 'activity_type',
530 'name' => 'Medical evaluation',
531 ])['id'];
532 $this->callApiSuccess('OptionValue', 'create', [
533 'id' => $activity_type_id,
534 'label' => 'Medical evaluation changed',
535 ]);
536
537 $p = new CRM_Case_XMLProcessor_Process();
538 $xml = $p->retrieve('housing_support');
539
540 // Test getting the `name`s
541 $activityTypes = $p->activityTypes($xml->ActivityTypes, FALSE, FALSE, FALSE);
542 $this->assertEquals(
543 [
544 13 => 'Open Case',
545 55 => 'Medical evaluation',
546 56 => 'Mental health evaluation',
547 57 => 'Secure temporary housing',
548 60 => 'Income and benefits stabilization',
549 58 => 'Long-term housing plan',
550 14 => 'Follow up',
551 15 => 'Change Case Type',
552 16 => 'Change Case Status',
553 18 => 'Change Case Start Date',
554 25 => 'Link Cases',
555 ],
556 $activityTypes
557 );
558
559 // While we're here and have the `name`s check the editable types in
560 // Settings.xml which is something that gets called reasonably often
561 // thru CRM_Case_XMLProcessor_Process::activityTypes().
562 $activityTypeValues = array_flip($activityTypes);
563 $xml = $p->retrieve('Settings');
564 $settings = $p->activityTypes($xml->ActivityTypes, FALSE, FALSE, 'edit');
565 $this->assertEquals(
566 [
567 'edit' => [
568 0 => $activityTypeValues['Change Case Status'],
569 1 => $activityTypeValues['Change Case Start Date'],
570 ],
571 ],
572 $settings
573 );
574
575 // Now get `label`s
576 $xml = $p->retrieve('housing_support');
577 $activityTypes = $p->activityTypes($xml->ActivityTypes, FALSE, TRUE, FALSE);
578 $this->assertEquals(
579 [
580 13 => 'Open Case',
581 55 => 'Medical evaluation changed',
582 56 => 'Mental health evaluation',
583 57 => 'Secure temporary housing',
584 60 => 'Income and benefits stabilization',
585 58 => 'Long-term housing plan',
586 14 => 'Follow up',
587 15 => 'Change Case Type',
588 16 => 'Change Case Status',
589 18 => 'Change Case Start Date',
590 25 => 'Link Cases',
591 ],
592 $activityTypes
593 );
594 }
595
596 }