Merge pull request #21599 from eileenmcnaughton/format
[civicrm-core.git] / tests / phpunit / CRM / Utils / TokenConsistencyTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Token\TokenProcessor;
13 use Civi\Api4\LocBlock;
14 use Civi\Api4\Email;
15 use Civi\Api4\Phone;
16 use Civi\Api4\Address;
17
18 /**
19 * CRM_Utils_TokenConsistencyTest
20 *
21 * Class for ensuring tokens have internal consistency.
22 *
23 * @group Tokens
24 *
25 * @group headless
26 */
27 class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
28
29 use CRMTraits_Custom_CustomDataTrait;
30
31 /**
32 * Created case.
33 *
34 * @var array
35 */
36 protected $case;
37
38 /**
39 * Recurring contribution.
40 *
41 * @var array
42 */
43 protected $contributionRecur;
44
45 /**
46 * Post test cleanup.
47 */
48 public function tearDown(): void {
49 $this->quickCleanup(['civicrm_case', 'civicrm_case_type', 'civicrm_participant', 'civicrm_event'], TRUE);
50 parent::tearDown();
51 }
52
53 /**
54 * Test that case tokens are consistently rendered.
55 *
56 * @throws \CiviCRM_API3_Exception
57 */
58 public function testCaseTokenConsistency(): void {
59 $this->createLoggedInUser();
60 CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
61 $this->createCustomGroupWithFieldOfType(['extends' => 'Case']);
62 $tokens = CRM_Core_SelectValues::caseTokens();
63 $this->assertEquals($this->getCaseTokens(), $tokens);
64 $caseID = $this->getCaseID();
65 $tokenString = $this->getTokenString(array_keys($this->getCaseTokens()));
66 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString, ['case' => $this->getCaseTokenKeys()]);
67 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
68 // Now do the same without passing in 'knownTokens'
69 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString);
70 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
71
72 // And check our deprecated tokens still work.
73 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, '{case.case_type_id} {case.status_id}');
74 $this->assertEquals('Housing Support Ongoing', $tokenHtml);
75
76 $additionalTokensFromProcessor = [
77 '{case.case_type_id}' => 'Case Type ID',
78 '{case.status_id}' => 'Case Status',
79 '{case.case_type_id:name}' => 'Machine name: Case Type',
80 '{case.status_id:name}' => 'Machine name: Case Status',
81 ];
82 $expectedTokens = array_merge($this->getCaseTokens(), $additionalTokensFromProcessor);
83
84 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
85 'controller' => __CLASS__,
86 'smarty' => FALSE,
87 'schema' => ['caseId'],
88 ]);
89 $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
90 $tokenProcessor->addRow([
91 'caseId' => $this->getCaseID(),
92 ]);
93 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
94
95 $tokenProcessor->evaluate();
96 foreach ($tokenProcessor->getRows() as $row) {
97 $text = $row->render('html');
98 }
99 $this->assertEquals($this->getExpectedCaseTokenOutput(), $text);
100 }
101
102 /**
103 * Get expected output from token parsing.
104 *
105 * @return string
106 */
107 protected function getExpectedCaseTokenOutput(): string {
108 return 'case.id :1
109 case.case_type_id:label :Housing Support
110 case.subject :Case Subject
111 case.start_date :July 23rd, 2021
112 case.end_date :July 26th, 2021
113 case.details :case details
114 case.status_id:label :Ongoing
115 case.is_deleted:label :No
116 case.created_date :' . CRM_Utils_Date::customFormat($this->case['created_date']) . '
117 case.modified_date :' . CRM_Utils_Date::customFormat($this->case['modified_date']) . '
118 case.custom_1 :' . '
119 ';
120 }
121
122 /**
123 * @return int
124 */
125 protected function getContactID(): int {
126 if (!isset($this->ids['Contact'][0])) {
127 $this->ids['Contact'][0] = $this->individualCreate();
128 }
129 return $this->ids['Contact'][0];
130 }
131
132 /**
133 * Get the keys for the case tokens.
134 *
135 * @return array
136 */
137 public function getCaseTokenKeys(): array {
138 $return = [];
139 foreach (array_keys($this->getCaseTokens()) as $key) {
140 $return[] = substr($key, 6, -1);
141 }
142 return $return;
143 }
144
145 /**
146 * Get declared tokens.
147 *
148 * @return string[]
149 */
150 public function getCaseTokens(): array {
151 return [
152 '{case.id}' => 'Case ID',
153 '{case.case_type_id:label}' => 'Case Type',
154 '{case.subject}' => 'Case Subject',
155 '{case.start_date}' => 'Case Start Date',
156 '{case.end_date}' => 'Case End Date',
157 '{case.details}' => 'Details',
158 '{case.status_id:label}' => 'Case Status',
159 '{case.is_deleted:label}' => 'Case is in the Trash',
160 '{case.created_date}' => 'Created Date',
161 '{case.modified_date}' => 'Modified Date',
162 '{case.custom_1}' => 'Enter text here :: Group with field text',
163 ];
164 }
165
166 /**
167 * Get case ID.
168 *
169 * @return int
170 */
171 protected function getCaseID(): int {
172 if (!isset($this->case)) {
173 $case_id = $this->callAPISuccess('Case', 'create', [
174 'case_type_id' => 'housing_support',
175 'activity_subject' => 'Case Subject',
176 'client_id' => $this->getContactID(),
177 'status_id' => 1,
178 'subject' => 'Case Subject',
179 'start_date' => '2021-07-23 15:39:20',
180 // Note end_date is inconsistent with status Ongoing but for the
181 // purposes of testing tokens is ok. Creating it with status Resolved
182 // then ignores our known fixed end date.
183 'end_date' => '2021-07-26 18:07:20',
184 'medium_id' => 2,
185 'details' => 'case details',
186 'activity_details' => 'blah blah',
187 'sequential' => 1,
188 ])['id'];
189 // Need to retrieve the case again because modified date might be updated a
190 // split-second later than the original return value because of activity
191 // triggers when the timeline is populated. The returned array from create
192 // is determined before that happens.
193 $this->case = $this->callAPISuccess('Case', 'getsingle', ['id' => $case_id]);
194 }
195 return $this->case['id'];
196 }
197
198 /**
199 * Test that contribution recur tokens are consistently rendered.
200 */
201 public function testContributionRecurTokenConsistency(): void {
202 $this->createLoggedInUser();
203 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
204 'controller' => __CLASS__,
205 'smarty' => FALSE,
206 'schema' => ['contribution_recurId'],
207 ]);
208 $this->assertEquals(array_merge($this->getContributionRecurTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
209 $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens()));
210
211 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
212 $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
213 $tokenProcessor->evaluate();
214 $this->assertEquals($this->getExpectedContributionRecurTokenOutPut(), $tokenProcessor->getRow(0)->render('html'));
215 }
216
217 /**
218 * Get tokens that are not advertised via listTokens.
219 *
220 * @return string[]
221 */
222 public function getUnadvertisedTokens(): array {
223 return [
224 '{membership.status_id}' => 'Status ID',
225 '{membership.membership_type_id}' => 'Membership Type ID',
226 '{membership.status_id:name}' => 'Machine name: Status',
227 '{membership.membership_type_id:name}' => 'Machine name: Membership Type',
228 ];
229 }
230
231 /**
232 * Get the contribution recur tokens keyed by the token.
233 *
234 * e.g {contribution_recur.id}
235 *
236 * @return array
237 */
238 protected function getContributionRecurTokens(): array {
239 $return = [];
240 foreach ($this->getContributionRecurTokensByField() as $key => $value) {
241 $return['{contribution_recur.' . $key . '}'] = $value;
242 }
243 return $return;
244 }
245
246 protected function getContributionRecurTokensByField(): array {
247 return [
248 'id' => 'Recurring Contribution ID',
249 'amount' => 'Amount',
250 'currency' => 'Currency',
251 'frequency_unit' => 'Frequency Unit',
252 'frequency_interval' => 'Interval (number of units)',
253 'installments' => 'Number of Installments',
254 'start_date' => 'Start Date',
255 'create_date' => 'Created Date',
256 'modified_date' => 'Modified Date',
257 'cancel_date' => 'Cancel Date',
258 'cancel_reason' => 'Cancellation Reason',
259 'end_date' => 'Recurring Contribution End Date',
260 'processor_id' => 'Processor ID',
261 'payment_token_id' => 'Payment Token ID',
262 'trxn_id' => 'Transaction ID',
263 'invoice_id' => 'Invoice ID',
264 'contribution_status_id' => 'Status',
265 'is_test:label' => 'Test',
266 'cycle_day' => 'Cycle Day',
267 'next_sched_contribution_date' => 'Next Scheduled Contribution Date',
268 'failure_count' => 'Number of Failures',
269 'failure_retry_date' => 'Retry Failed Attempt Date',
270 'auto_renew:label' => 'Auto Renew',
271 'payment_processor_id' => 'Payment Processor ID',
272 'financial_type_id' => 'Financial Type ID',
273 'payment_instrument_id' => 'Payment Method',
274 'is_email_receipt:label' => 'Send email Receipt?',
275 'frequency_unit:label' => 'Frequency Unit',
276 'frequency_unit:name' => 'Machine name: Frequency Unit',
277 'contribution_status_id:label' => 'Status',
278 'contribution_status_id:name' => 'Machine name: Status',
279 'payment_processor_id:label' => 'Payment Processor',
280 'payment_processor_id:name' => 'Machine name: Payment Processor',
281 'financial_type_id:label' => 'Financial Type',
282 'financial_type_id:name' => 'Machine name: Financial Type',
283 'payment_instrument_id:label' => 'Payment Method',
284 'payment_instrument_id:name' => 'Machine name: Payment Method',
285 ];
286 }
287
288 /**
289 * Get contributionRecur ID.
290 *
291 * @return int
292 */
293 protected function getContributionRecurID(): int {
294 if (!isset($this->contributionRecur)) {
295 $paymentProcessorID = $this->processorCreate();
296 $this->contributionRecur = $this->callAPISuccess('ContributionRecur', 'create', [
297 'contact_id' => $this->getContactID(),
298 'status_id' => 1,
299 'is_email_receipt' => 1,
300 'start_date' => '2021-07-23 15:39:20',
301 'end_date' => '2021-07-26 18:07:20',
302 'cancel_date' => '2021-08-19 09:12:45',
303 'next_sched_contribution_date' => '2021-09-08',
304 'cancel_reason' => 'Because',
305 'amount' => 5990.99,
306 'currency' => 'EUR',
307 'frequency_unit' => 'year',
308 'frequency_interval' => 2,
309 'installments' => 24,
310 'payment_instrument_id' => 'Check',
311 'financial_type_id' => 'Member dues',
312 'processor_id' => 'abc',
313 'payment_processor_id' => $paymentProcessorID,
314 'trxn_id' => 123,
315 'invoice_id' => 'inv123',
316 'sequential' => 1,
317 'failure_retry_date' => '2020-01-03',
318 'auto_renew' => 1,
319 'cycle_day' => '15',
320 'is_test' => TRUE,
321 'payment_token_id' => $this->callAPISuccess('PaymentToken', 'create', [
322 'contact_id' => $this->getContactID(),
323 'token' => 456,
324 'payment_processor_id' => $paymentProcessorID,
325 ])['id'],
326 ])['values'][0];
327 }
328 return $this->contributionRecur['id'];
329 }
330
331 /**
332 * Get rendered output for contribution tokens.
333 *
334 * @return string
335 */
336 protected function getExpectedContributionRecurTokenOutPut(): string {
337 return 'contribution_recur.id :' . $this->getContributionRecurID() . '
338 contribution_recur.amount :€ 5,990.99
339 contribution_recur.currency :EUR
340 contribution_recur.frequency_unit :year
341 contribution_recur.frequency_interval :2
342 contribution_recur.installments :24
343 contribution_recur.start_date :July 23rd, 2021 3:39 PM
344 contribution_recur.create_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['create_date']) . '
345 contribution_recur.modified_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['modified_date']) . '
346 contribution_recur.cancel_date :August 19th, 2021 9:12 AM
347 contribution_recur.cancel_reason :Because
348 contribution_recur.end_date :July 26th, 2021 6:07 PM
349 contribution_recur.processor_id :abc
350 contribution_recur.payment_token_id :1
351 contribution_recur.trxn_id :123
352 contribution_recur.invoice_id :inv123
353 contribution_recur.contribution_status_id :2
354 contribution_recur.is_test:label :Yes
355 contribution_recur.cycle_day :15
356 contribution_recur.next_sched_contribution_date :September 8th, 2021
357 contribution_recur.failure_count :0
358 contribution_recur.failure_retry_date :January 3rd, 2020
359 contribution_recur.auto_renew:label :Yes
360 contribution_recur.payment_processor_id :1
361 contribution_recur.financial_type_id :2
362 contribution_recur.payment_instrument_id :4
363 contribution_recur.is_email_receipt:label :Yes
364 contribution_recur.frequency_unit:label :year
365 contribution_recur.frequency_unit:name :year
366 contribution_recur.contribution_status_id:label :Pending Label**
367 contribution_recur.contribution_status_id:name :Pending
368 contribution_recur.payment_processor_id:label :Dummy (test)
369 contribution_recur.payment_processor_id:name :Dummy (test)
370 contribution_recur.financial_type_id:label :Member Dues
371 contribution_recur.financial_type_id:name :Member Dues
372 contribution_recur.payment_instrument_id:label :Check
373 contribution_recur.payment_instrument_id:name :Check
374 ';
375
376 }
377
378 /**
379 * Test that membership tokens are consistently rendered.
380 *
381 * @throws \API_Exception
382 */
383 public function testMembershipTokenConsistency(): void {
384 $this->createLoggedInUser();
385 $this->restoreMembershipTypes();
386 $this->createCustomGroupWithFieldOfType(['extends' => 'Membership']);
387 $tokens = CRM_Core_SelectValues::membershipTokens();
388 $expectedTokens = $this->getMembershipTokens();
389 $this->assertEquals($expectedTokens, $tokens);
390 $newStyleTokens = "\n{membership.status_id:label}\n{membership.membership_type_id:label}\n";
391 $tokenString = $newStyleTokens . implode("\n", array_keys($this->getMembershipTokens()));
392
393 $memberships = CRM_Utils_Token::getMembershipTokenDetails([$this->getMembershipID()]);
394 $messageToken = CRM_Utils_Token::getTokens($tokenString);
395 $tokenHtml = CRM_Utils_Token::replaceEntityTokens('membership', $memberships[$this->getMembershipID()], $tokenString, $messageToken);
396 $this->assertEquals($this->getExpectedMembershipTokenOutput(), $tokenHtml);
397
398 // Custom fields work in the processor so test it....
399 $tokenString .= "\n{membership." . $this->getCustomFieldName('text') . '}';
400 // Now compare with scheduled reminder
401 $mut = new CiviMailUtils($this);
402 CRM_Utils_Time::setTime('2007-01-22 15:00:00');
403 $this->callAPISuccess('action_schedule', 'create', [
404 'title' => 'job',
405 'subject' => 'job',
406 'entity_value' => 1,
407 'mapping_id' => 4,
408 'start_action_date' => 'membership_join_date',
409 'start_action_offset' => 1,
410 'start_action_condition' => 'after',
411 'start_action_unit' => 'day',
412 'body_html' => $tokenString,
413 ]);
414 $this->callAPISuccess('job', 'send_reminder', []);
415 $expected = $this->getExpectedMembershipTokenOutput();
416 // Unlike the legacy method custom fields are resolved by the processor.
417 $expected .= "\nmy field";
418 $mut->checkMailLog([$expected]);
419
420 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
421 'controller' => __CLASS__,
422 'smarty' => FALSE,
423 'schema' => ['membershipId'],
424 ]);
425 $tokens = $tokenProcessor->listTokens();
426 // Add in custom tokens as token processor supports these.
427 $expectedTokens['{membership.custom_1}'] = 'Enter text here :: Group with field text';
428 // Add in unadvertised tokens - for now they are included.
429 $expectedTokens = array_merge($expectedTokens, $this->getUnadvertisedTokens());
430 $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens);
431 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
432 $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]);
433 $tokenProcessor->evaluate();
434 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
435
436 }
437
438 /**
439 * Get declared membership tokens.
440 *
441 * @return string[]
442 */
443 public function getMembershipTokens(): array {
444 return [
445 '{membership.id}' => 'Membership ID',
446 '{membership.status_id:label}' => 'Status',
447 '{membership.membership_type_id:label}' => 'Membership Type',
448 '{membership.start_date}' => 'Membership Start Date',
449 '{membership.join_date}' => 'Member Since',
450 '{membership.end_date}' => 'Membership Expiration Date',
451 '{membership.fee}' => 'Membership Fee',
452 ];
453 }
454
455 /**
456 * Get case ID.
457 *
458 * @return int
459 */
460 protected function getMembershipID(): int {
461 if (!isset($this->ids['Membership'][0])) {
462 $this->ids['Membership'][0] = $this->contactMembershipCreate([
463 'contact_id' => $this->getContactID(),
464 $this->getCustomFieldName('text') => 'my field',
465 ]);
466 }
467 return $this->ids['Membership'][0];
468 }
469
470 /**
471 * Get expected output from token parsing.
472 *
473 * @return string
474 */
475 protected function getExpectedParticipantTokenOutput(): string {
476 return 'participant.status_id :2
477 participant.role_id :1
478 participant.register_date :February 19th, 2007
479 participant.source :Wimbeldon
480 participant.fee_level :steep
481 participant.fee_amount :$ 50.00
482 participant.registered_by_id :
483 participant.transferred_to_contact_id :
484 participant.role_id:label :Attendee
485 participant.balance :
486 participant.custom_2 :99999
487 participant.id :2
488 participant.fee_currency :USD
489 participant.discount_amount :
490 participant.status_id:label :Attended
491 participant.status_id:name :Attended
492 participant.role_id:name :Attendee
493 participant.is_test:label :No
494 participant.must_wait :
495 ';
496 }
497
498 /**
499 * Get expected output from token parsing.
500 *
501 * @return string
502 */
503 protected function getExpectedEventTokenOutput(): string {
504 return 'event.id :' . $this->ids['event'][0] . '
505 event.title :Annual CiviCRM meet
506 event.start_date :October 21st, 2008
507 event.end_date :October 23rd, 2008
508 event.event_type_id:label :Conference
509 event.summary :If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
510 event.contact_email :event@example.com
511 event.contact_phone :456 789
512 event.description :event description
513 event.location :15 Walton St
514 Emerald City, Maine 90210
515
516 event.info_url :' . CRM_Utils_System::url('civicrm/event/info', NULL, TRUE) . '&reset=1&id=1
517 event.registration_url :' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
518 event.custom_1 :my field
519 ';
520 }
521
522 /**
523 * Get expected output from token parsing.
524 *
525 * @return string
526 */
527 protected function getExpectedMembershipTokenOutput(): string {
528 return '
529 Expired
530 General
531 1
532 Expired
533 General
534 January 21st, 2007
535 January 21st, 2007
536 December 21st, 2007
537 100.00';
538 }
539
540 /**
541 * Test that membership tokens are consistently rendered.
542 *
543 * @throws \API_Exception
544 */
545 public function testParticipantTokenConsistency(): void {
546 $this->createLoggedInUser();
547 $this->setupParticipantScheduledReminder();
548
549 $tokens = CRM_Core_SelectValues::participantTokens();
550 $this->assertEquals($this->getParticipantTokens(), $tokens);
551
552 $mut = new CiviMailUtils($this);
553
554 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
555 'controller' => __CLASS__,
556 'smarty' => FALSE,
557 'schema' => ['participantId'],
558 ]);
559 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
560
561 $this->callAPISuccess('job', 'send_reminder', []);
562 $expected = $this->getExpectedParticipantTokenOutput();
563 $mut->checkMailLog([$expected]);
564
565 $tokenProcessor->addMessage('html', $this->getTokenString(array_keys($this->getParticipantTokens())), 'text/plain');
566 $tokenProcessor->addRow(['participantId' => $this->ids['participant'][0]]);
567 $tokenProcessor->evaluate();
568 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
569
570 }
571
572 /**
573 * Test that membership tokens are consistently rendered.
574 *
575 * @throws \API_Exception
576 * @throws \CRM_Core_Exception
577 */
578 public function testParticipantCustomDateToken(): void {
579 $this->createEventAndParticipant();
580 $dateFieldID = $this->createDateCustomField(['custom_group_id' => $this->ids['CustomGroup']['participant_'], 'default_value' => ''])['id'];
581 $input = '{participant.custom_' . $dateFieldID . '}';
582 $input .= '{participant.' . $this->getCustomFieldName('participant_int') . '}';
583 $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
584 'messageTemplate' => ['msg_html' => $input],
585 'tokenContext' => array_merge(['participantId' => $this->ids['participant'][0]], ['schema' => ['participantId', 'eventId']]),
586 ])['html'];
587 $this->assertEquals(99999, $tokenHtml);
588 }
589
590 /**
591 * Get declared participant tokens.
592 *
593 * @return string[]
594 */
595 public function getParticipantTokens(): array {
596 return [
597 '{participant.status_id}' => 'Status ID',
598 '{participant.role_id}' => 'Participant Role ID',
599 '{participant.register_date}' => 'Register date',
600 '{participant.source}' => 'Participant Source',
601 '{participant.fee_level}' => 'Fee level',
602 '{participant.fee_amount}' => 'Fee Amount',
603 '{participant.registered_by_id}' => 'Registered By Participant ID',
604 '{participant.transferred_to_contact_id}' => 'Transferred to Contact ID',
605 '{participant.role_id:label}' => 'Participant Role',
606 '{participant.balance}' => 'Event Balance',
607 '{participant.' . $this->getCustomFieldName('participant_int') . '}' => 'Enter integer here :: participant_Group with field int',
608 '{participant.id}' => 'Participant ID',
609 '{participant.fee_currency}' => 'Fee Currency',
610 '{participant.discount_amount}' => 'Discount Amount',
611 '{participant.status_id:label}' => 'Status',
612 '{participant.status_id:name}' => 'Machine name: Status',
613 '{participant.role_id:name}' => 'Machine name: Participant Role',
614 '{participant.is_test:label}' => 'Test',
615 '{participant.must_wait}' => 'Must Wait on List',
616 ];
617 }
618
619 /**
620 * Test that domain tokens are consistently rendered.
621 */
622 public function testDomainTokenConsistency(): void {
623 $tokens = CRM_Core_SelectValues::domainTokens();
624 $this->assertEquals($this->getDomainTokens(), $tokens);
625 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
626 'controller' => __CLASS__,
627 'smarty' => FALSE,
628 ]);
629 $tokens['{domain.id}'] = 'Domain ID';
630 $tokens['{domain.description}'] = 'Domain Description';
631 $tokens['{domain.now}'] = 'Current time/date';
632 $this->assertEquals($tokens, $tokenProcessor->listTokens());
633 }
634
635 /**
636 * @throws \API_Exception
637 * @throws \CRM_Core_Exception
638 */
639 public function testDomainNow(): void {
640 putenv('TIME_FUNC=frozen');
641 CRM_Utils_Time::setTime('2021-09-18 23:58:00');
642 $modifiers = [
643 'shortdate' => '09/18/2021',
644 '%B %Y' => 'September 2021',
645 ];
646 foreach ($modifiers as $filter => $expected) {
647 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
648 'messageTemplate' => [
649 'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
650 ],
651 ])['text'];
652 $this->assertEquals($expected, $resolved);
653 }
654 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
655 'messageTemplate' => [
656 'msg_text' => '{domain.now}',
657 ],
658 ])['text'];
659 $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
660
661 // This example is malformed - no quotes
662 try {
663 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
664 'messageTemplate' => [
665 'msg_text' => '{domain.now|crmDate:shortdate}',
666 ],
667 ])['text'];
668 $this->fail('Expected unquoted parameter to fail');
669 }
670 catch (\CRM_Core_Exception $e) {
671 $this->assertRegExp(';Malformed token param;', $e->getMessage());
672 }
673 }
674
675 /**
676 * Get declared participant tokens.
677 *
678 * @return string[]
679 */
680 public function getDomainTokens(): array {
681 return [
682 '{domain.name}' => ts('Domain name'),
683 '{domain.address}' => ts('Domain (organization) address'),
684 '{domain.phone}' => ts('Domain (organization) phone'),
685 '{domain.email}' => 'Domain (organization) email',
686 '{domain.id}' => ts('Domain ID'),
687 '{domain.description}' => ts('Domain Description'),
688 '{domain.now}' => 'Current time/date',
689 ];
690 }
691
692 /**
693 * Test that event tokens are consistently rendered.
694 *
695 * @throws \API_Exception
696 */
697 public function testEventTokenConsistency(): void {
698 $mut = new CiviMailUtils($this);
699 $this->setupParticipantScheduledReminder();
700
701 $tokens = CRM_Core_SelectValues::eventTokens();
702 $unadvertisedTokens = [
703 '{event.event_type_id}' => 'Event Type',
704 '{event.event_type_id:name}' => 'Machine name: Event Type',
705 ];
706 $this->assertEquals(array_merge($this->getEventTokens(), $unadvertisedTokens), $tokens);
707 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
708 'controller' => __CLASS__,
709 'smarty' => FALSE,
710 'schema' => ['eventId'],
711 ]);
712 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
713
714 $expectedEventString = $this->getExpectedEventTokenOutput();
715 $this->callAPISuccess('job', 'send_reminder', []);
716 $expectedParticipantString = $this->getExpectedParticipantTokenOutput();
717 $toCheck = array_merge(explode("\n", $expectedEventString), explode("\n", $expectedParticipantString));
718 $toCheck[] = $expectedEventString;
719 $toCheck[] = $expectedParticipantString;
720 $mut->checkMailLog($toCheck);
721 $tokens = array_keys($this->getEventTokens());
722 $html = $this->getTokenString($tokens);
723 $tokenProcessor->addMessage('html', $html, 'text/plain');
724 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
725 $tokenProcessor->evaluate();
726 $this->assertEquals($expectedEventString, $tokenProcessor->getRow(0)->render('html'));
727 }
728
729 /**
730 * Test that event tokens work absent participant tokens.
731 *
732 * @throws \API_Exception
733 */
734 public function testEventTokenConsistencyNoParticipantTokens(): void {
735 $mut = new CiviMailUtils($this);
736 $this->setupParticipantScheduledReminder(FALSE);
737
738 $this->callAPISuccess('job', 'send_reminder', []);
739 $expected = $this->getExpectedEventTokenOutput();
740 // Checking these individually is easier to decipher discrepancies
741 // but we also want to check in entirety.
742 $toCheck = explode("\n", $expected);
743 $toCheck[] = $expected;
744 $mut->checkMailLog($toCheck);
745
746 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
747 'controller' => __CLASS__,
748 'smarty' => FALSE,
749 'schema' => ['eventId'],
750 ]);
751 $html = $this->getTokenString(array_keys($this->getEventTokens()));
752
753 $tokenProcessor->addMessage('html', $html, 'text/plain');
754 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
755 $tokenProcessor->evaluate();
756 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
757
758 }
759
760 /**
761 * Set up scheduled reminder for participants.
762 *
763 * @throws \API_Exception
764 */
765 public function setupParticipantScheduledReminder($includeParticipant = TRUE): void {
766 $this->createEventAndParticipant();
767 $tokens = array_keys($this->getEventTokens());
768 if ($includeParticipant) {
769 $tokens = array_keys(array_merge($this->getEventTokens(), $this->getParticipantTokens()));
770 }
771 $html = $this->getTokenString($tokens);
772 CRM_Utils_Time::setTime('2007-02-20 15:00:00');
773 $this->callAPISuccess('action_schedule', 'create', [
774 'title' => 'job',
775 'subject' => 'job',
776 'entity_value' => 1,
777 'mapping_id' => 2,
778 'start_action_date' => 'register_date',
779 'start_action_offset' => 1,
780 'start_action_condition' => 'after',
781 'start_action_unit' => 'day',
782 'body_html' => $html,
783 ]);
784 }
785
786 /**
787 * Get expected event tokens.
788 *
789 * @return string[]
790 */
791 protected function getEventTokens(): array {
792 return [
793 '{event.id}' => 'Event ID',
794 '{event.title}' => 'Event Title',
795 '{event.start_date}' => 'Event Start Date',
796 '{event.end_date}' => 'Event End Date',
797 '{event.event_type_id:label}' => 'Event Type',
798 '{event.summary}' => 'Event Summary',
799 '{event.contact_email}' => 'Event Contact Email',
800 '{event.contact_phone}' => 'Event Contact Phone',
801 '{event.description}' => 'Event Description',
802 '{event.location}' => 'Event Location',
803 '{event.info_url}' => 'Event Info URL',
804 '{event.registration_url}' => 'Event Registration URL',
805 '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
806 ];
807 }
808
809 /**
810 * @param array $tokens
811 *
812 * @return string
813 */
814 protected function getTokenString(array $tokens): string {
815 $html = '';
816 foreach ($tokens as $token) {
817 $html .= substr($token, 1, -1) . ' :' . $token . "\n";
818 }
819 return $html;
820 }
821
822 /**
823 * Create an event with a participant.
824 *
825 * @throws \API_Exception
826 */
827 protected function createEventAndParticipant(): void {
828 $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
829 $this->createCustomGroupWithFieldOfType(['extends' => 'Participant'], 'int', 'participant_');
830 $emailID = Email::create()
831 ->setValues(['email' => 'event@example.com'])
832 ->execute()
833 ->first()['id'];
834 $addressID = Address::create()->setValues([
835 'street_address' => '15 Walton St',
836 'supplemental_address_1' => 'up the road',
837 'city' => 'Emerald City',
838 'state_province_id:label' => 'Maine',
839 'postal_code' => 90210,
840 ])->execute()->first()['id'];
841 $phoneID = Phone::create()
842 ->setValues(['phone' => '456 789'])
843 ->execute()
844 ->first()['id'];
845
846 $locationBlockID = LocBlock::save(FALSE)->setRecords([
847 [
848 'email_id' => $emailID,
849 'address_id' => $addressID,
850 'phone_id' => $phoneID,
851 ],
852 ])->execute()->first()['id'];
853 $this->ids['event'][0] = $this->eventCreate([
854 'description' => 'event description',
855 $this->getCustomFieldName('text') => 'my field',
856 'loc_block_id' => $locationBlockID,
857 ])['id'];
858 // Create an unrelated participant record so that the ids don't match.
859 // this prevents things working just because the id 'happens to be valid'
860 $this->participantCreate([
861 'register_date' => '2020-01-01',
862 'event_id' => $this->ids['event'][0],
863 ]);
864 $this->ids['participant'][0] = $this->participantCreate([
865 'event_id' => $this->ids['event'][0],
866 'fee_amount' => 50,
867 'fee_level' => 'steep',
868 $this->getCustomFieldName('participant_int') => '99999',
869 ]);
870 }
871
872 }