revert 20476
[civicrm-core.git] / tests / phpunit / CRM / Utils / TokenConsistencyTest.php
CommitLineData
d0ce76fd
EM
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
0f4031da 12use Civi\Token\TokenProcessor;
f399fbd8
EM
13use Civi\Api4\LocBlock;
14use Civi\Api4\Email;
15use Civi\Api4\Phone;
16use Civi\Api4\Address;
0f4031da 17
d0ce76fd
EM
18/**
19 * CRM_Utils_TokenConsistencyTest
20 *
21 * Class for ensuring tokens have internal consistency.
22 *
23 * @group Tokens
24 *
25 * @group headless
26 */
27class 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
0f4031da
EM
38 /**
39 * Recurring contribution.
40 *
41 * @var array
42 */
43 protected $contributionRecur;
44
d0ce76fd
EM
45 /**
46 * Post test cleanup.
d0ce76fd
EM
47 */
48 public function tearDown(): void {
e80f2261 49 $this->quickCleanup(['civicrm_case', 'civicrm_case_type', 'civicrm_participant', 'civicrm_event'], TRUE);
f70a513f 50 $this->quickCleanUpFinancialEntities();
167f848f
TO
51
52 // WORKAROUND: CRM_Event_Tokens copies `civicrm_event` data into metadata cache. That should probably change, but that's a different scope-of-work.
53 // `clear()` works around it. This should be removed if that's updated, but it will be safe either way.
54 Civi::cache('metadata')->clear();
55
d0ce76fd
EM
56 parent::tearDown();
57 }
58
59 /**
60 * Test that case tokens are consistently rendered.
61 *
d0ce76fd
EM
62 * @throws \CiviCRM_API3_Exception
63 */
64 public function testCaseTokenConsistency(): void {
65 $this->createLoggedInUser();
66 CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
67 $this->createCustomGroupWithFieldOfType(['extends' => 'Case']);
68 $tokens = CRM_Core_SelectValues::caseTokens();
69 $this->assertEquals($this->getCaseTokens(), $tokens);
70 $caseID = $this->getCaseID();
044c0ad1 71 $tokenString = $this->getTokenString(array_keys($this->getCaseTokens()));
78ffc4d7 72 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString, ['case' => $this->getCaseTokenKeys()]);
d0ce76fd
EM
73 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
74 // Now do the same without passing in 'knownTokens'
78ffc4d7 75 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString);
d0ce76fd 76 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
2f5c0408
EM
77
78 // And check our deprecated tokens still work.
79 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, '{case.case_type_id} {case.status_id}');
80 $this->assertEquals('Housing Support Ongoing', $tokenHtml);
78ffc4d7
EM
81 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
82 'controller' => __CLASS__,
83 'smarty' => FALSE,
84 'schema' => ['caseId'],
85 ]);
889b0617 86 $this->assertEquals(array_merge($this->getCaseTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
78ffc4d7
EM
87 $tokenProcessor->addRow([
88 'caseId' => $this->getCaseID(),
89 ]);
90 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
91
92 $tokenProcessor->evaluate();
93 foreach ($tokenProcessor->getRows() as $row) {
94 $text = $row->render('html');
95 }
96 $this->assertEquals($this->getExpectedCaseTokenOutput(), $text);
d0ce76fd
EM
97 }
98
99 /**
100 * Get expected output from token parsing.
101 *
102 * @return string
103 */
104 protected function getExpectedCaseTokenOutput(): string {
044c0ad1
EM
105 return 'case.id :1
106case.case_type_id:label :Housing Support
107case.subject :Case Subject
108case.start_date :July 23rd, 2021
109case.end_date :July 26th, 2021
110case.details :case details
111case.status_id:label :Ongoing
112case.is_deleted:label :No
113case.created_date :' . CRM_Utils_Date::customFormat($this->case['created_date']) . '
114case.modified_date :' . CRM_Utils_Date::customFormat($this->case['modified_date']) . '
115case.custom_1 :' . '
d0ce76fd
EM
116';
117 }
118
119 /**
120 * @return int
121 */
122 protected function getContactID(): int {
123 if (!isset($this->ids['Contact'][0])) {
124 $this->ids['Contact'][0] = $this->individualCreate();
125 }
126 return $this->ids['Contact'][0];
127 }
128
129 /**
130 * Get the keys for the case tokens.
131 *
132 * @return array
133 */
134 public function getCaseTokenKeys(): array {
135 $return = [];
136 foreach (array_keys($this->getCaseTokens()) as $key) {
137 $return[] = substr($key, 6, -1);
138 }
139 return $return;
140 }
141
142 /**
143 * Get declared tokens.
144 *
145 * @return string[]
146 */
147 public function getCaseTokens(): array {
148 return [
149 '{case.id}' => 'Case ID',
2f5c0408 150 '{case.case_type_id:label}' => 'Case Type',
d0ce76fd
EM
151 '{case.subject}' => 'Case Subject',
152 '{case.start_date}' => 'Case Start Date',
153 '{case.end_date}' => 'Case End Date',
154 '{case.details}' => 'Details',
2f5c0408 155 '{case.status_id:label}' => 'Case Status',
78ffc4d7 156 '{case.is_deleted:label}' => 'Case is in the Trash',
d0ce76fd
EM
157 '{case.created_date}' => 'Created Date',
158 '{case.modified_date}' => 'Modified Date',
159 '{case.custom_1}' => 'Enter text here :: Group with field text',
160 ];
161 }
162
163 /**
164 * Get case ID.
165 *
166 * @return int
167 */
168 protected function getCaseID(): int {
169 if (!isset($this->case)) {
a0fb3068 170 $case_id = $this->callAPISuccess('Case', 'create', [
d0ce76fd
EM
171 'case_type_id' => 'housing_support',
172 'activity_subject' => 'Case Subject',
173 'client_id' => $this->getContactID(),
174 'status_id' => 1,
175 'subject' => 'Case Subject',
176 'start_date' => '2021-07-23 15:39:20',
a0fb3068 177 // Note end_date is inconsistent with status Ongoing but for the
178 // purposes of testing tokens is ok. Creating it with status Resolved
179 // then ignores our known fixed end date.
d0ce76fd
EM
180 'end_date' => '2021-07-26 18:07:20',
181 'medium_id' => 2,
182 'details' => 'case details',
183 'activity_details' => 'blah blah',
184 'sequential' => 1,
a0fb3068 185 ])['id'];
186 // Need to retrieve the case again because modified date might be updated a
187 // split-second later than the original return value because of activity
188 // triggers when the timeline is populated. The returned array from create
189 // is determined before that happens.
190 $this->case = $this->callAPISuccess('Case', 'getsingle', ['id' => $case_id]);
d0ce76fd
EM
191 }
192 return $this->case['id'];
193 }
194
0f4031da
EM
195 /**
196 * Test that contribution recur tokens are consistently rendered.
197 */
198 public function testContributionRecurTokenConsistency(): void {
199 $this->createLoggedInUser();
200 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
201 'controller' => __CLASS__,
202 'smarty' => FALSE,
203 'schema' => ['contribution_recurId'],
204 ]);
889b0617
EM
205 $expectedTokens = array_merge($this->getContributionRecurTokens(), $this->getDomainTokens());
206 $this->assertEquals(array_diff_key($expectedTokens, $this->getUnadvertisedTokens()), $tokenProcessor->listTokens());
044c0ad1 207 $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens()));
0f4031da
EM
208
209 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
210 $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
211 $tokenProcessor->evaluate();
212 $this->assertEquals($this->getExpectedContributionRecurTokenOutPut(), $tokenProcessor->getRow(0)->render('html'));
213 }
214
b0a68d88
EM
215 /**
216 * Test that contribution recur tokens are consistently rendered.
217 */
218 public function testContributionRecurTokenRaw(): void {
219 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
220 'controller' => __CLASS__,
221 'smarty' => FALSE,
222 'schema' => ['contribution_recurId'],
223 ]);
224 $tokenProcessor->addMessage('not_specified', '{contribution_recur.amount}', 'text/plain');
225 $tokenProcessor->addMessage('money', '{contribution_recur.amount|crmMoney}', 'text/plain');
226 $tokenProcessor->addMessage('raw', '{contribution_recur.amount|raw}', 'text/plain');
227 $tokenProcessor->addMessage('moneyNumber', '{contribution_recur.amount|crmMoneyNumber}', 'text/plain');
228 $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
229 $tokenProcessor->evaluate();
230 $this->assertEquals('€5,990.99', $tokenProcessor->getRow(0)->render('not_specified'));
231 $this->assertEquals('€5,990.99', $tokenProcessor->getRow(0)->render('money'));
232 $this->assertEquals('5990.99', $tokenProcessor->getRow(0)->render('raw'));
233 }
234
f70a513f
EM
235 /**
236 * Test money format tokens can respect passed in locale.
237 */
238 public function testMoneyFormat(): void {
239 // Our 'migration' off configured thousand separators at the moment is a define.
240 putenv('IGNORE_SEPARATOR_CONFIG=1');
241 $this->createLoggedInUser();
242 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
243 'controller' => __CLASS__,
244 'smarty' => FALSE,
245 'schema' => ['contribution_recurId'],
246 ]);
247 $tokenString = '{contribution_recur.amount}';
248 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
249 $tokenProcessor->addRow([
250 'contribution_recurId' => $this->getContributionRecurID(),
251 'locale' => 'nb_NO',
252 ]);
253 $tokenProcessor->evaluate();
254 $this->assertEquals('€ 5 990,99', $tokenProcessor->getRow(0)->render('html'));
255 }
256
e9841a51
EM
257 /**
258 * Get tokens that are not advertised via listTokens.
259 *
260 * @return string[]
261 */
262 public function getUnadvertisedTokens(): array {
263 return [
264 '{membership.status_id}' => 'Status ID',
265 '{membership.membership_type_id}' => 'Membership Type ID',
266 '{membership.status_id:name}' => 'Machine name: Status',
267 '{membership.membership_type_id:name}' => 'Machine name: Membership Type',
889b0617
EM
268 '{contribution_recur.frequency_unit}' => 'Frequency Unit',
269 '{contribution_recur.contribution_status_id}' => 'Status',
270 '{contribution_recur.payment_processor_id}' => 'Payment Processor ID',
271 '{contribution_recur.financial_type_id}' => 'Financial Type ID',
272 '{contribution_recur.payment_instrument_id}' => 'Payment Method',
273 '{contribution_recur.frequency_unit:name}' => 'Machine name: Frequency Unit',
274 '{contribution_recur.payment_instrument_id:name}' => 'Machine name: Payment Method',
275 '{contribution_recur.contribution_status_id:name}' => 'Machine name: Status',
276 '{contribution_recur.payment_processor_id:name}' => 'Machine name: Payment Processor',
277 '{contribution_recur.financial_type_id:name}' => 'Machine name: Financial Type',
278 '{participant.status_id:name}' => 'Machine name: Status',
279 '{participant.role_id:name}' => 'Machine name: Participant Role',
280 '{participant.status_id}' => 'Status ID',
281 '{participant.role_id}' => 'Participant Role ID',
e9841a51
EM
282 ];
283 }
284
d49e8eec
EM
285 /**
286 * Test tokens in 2 ways to ensure consistent handling.
287 *
288 * 1) as part of the greeting processing
289 * 2) via the token processor.
290 *
291 */
292 public function testOddTokens(): void {
293
294 $variants = [
295 [
296 'string' => '{contact.individual_prefix}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.individual_suffix}',
297 'expected' => 'Mr. Anthony Anderson II',
298 ],
299 [
300 'string' => '{contact.prefix_id:label}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.suffix_id:label}',
301 'expected' => 'Mr. Anthony Anderson II',
302 ],
303 ];
304 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
305 'smarty' => FALSE,
306 'schema' => ['contactId'],
307 ]);
308 $contactID = $this->individualCreate(['middle_name' => '']);
309 $tokenProcessor->addRow(['contactId' => $contactID]);
d49e8eec
EM
310 foreach ($variants as $index => $variant) {
311 $tokenProcessor->addMessage($index, $variant['string'], 'text/plain');
312 }
313 $tokenProcessor->evaluate();
314 $result = $tokenProcessor->getRow(0);
315 foreach ($variants as $index => $variant) {
316 $greetingString = $variant['string'];
317 CRM_Utils_Token::replaceGreetingTokens($greetingString, $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]), $contactID);
0bb18246
TO
318 $this->assertEquals($variant['expected'], $greetingString, 'replaceGreetingTokens() should render expected output');
319 $this->assertEquals($variant['expected'], $result->render($index), 'TokenProcessor should render expected output');
d49e8eec
EM
320 }
321 }
322
0f4031da
EM
323 /**
324 * Get the contribution recur tokens keyed by the token.
325 *
326 * e.g {contribution_recur.id}
327 *
328 * @return array
329 */
330 protected function getContributionRecurTokens(): array {
331 $return = [];
332 foreach ($this->getContributionRecurTokensByField() as $key => $value) {
333 $return['{contribution_recur.' . $key . '}'] = $value;
334 }
335 return $return;
336 }
337
338 protected function getContributionRecurTokensByField(): array {
339 return [
340 'id' => 'Recurring Contribution ID',
341 'amount' => 'Amount',
342 'currency' => 'Currency',
343 'frequency_unit' => 'Frequency Unit',
344 'frequency_interval' => 'Interval (number of units)',
345 'installments' => 'Number of Installments',
346 'start_date' => 'Start Date',
347 'create_date' => 'Created Date',
348 'modified_date' => 'Modified Date',
349 'cancel_date' => 'Cancel Date',
350 'cancel_reason' => 'Cancellation Reason',
351 'end_date' => 'Recurring Contribution End Date',
352 'processor_id' => 'Processor ID',
353 'payment_token_id' => 'Payment Token ID',
354 'trxn_id' => 'Transaction ID',
355 'invoice_id' => 'Invoice ID',
356 'contribution_status_id' => 'Status',
97ca72e4 357 'is_test:label' => 'Test',
0f4031da
EM
358 'cycle_day' => 'Cycle Day',
359 'next_sched_contribution_date' => 'Next Scheduled Contribution Date',
360 'failure_count' => 'Number of Failures',
361 'failure_retry_date' => 'Retry Failed Attempt Date',
97ca72e4 362 'auto_renew:label' => 'Auto Renew',
0f4031da
EM
363 'payment_processor_id' => 'Payment Processor ID',
364 'financial_type_id' => 'Financial Type ID',
365 'payment_instrument_id' => 'Payment Method',
97ca72e4 366 'is_email_receipt:label' => 'Send email Receipt?',
0f4031da
EM
367 'frequency_unit:label' => 'Frequency Unit',
368 'frequency_unit:name' => 'Machine name: Frequency Unit',
369 'contribution_status_id:label' => 'Status',
370 'contribution_status_id:name' => 'Machine name: Status',
371 'payment_processor_id:label' => 'Payment Processor',
372 'payment_processor_id:name' => 'Machine name: Payment Processor',
373 'financial_type_id:label' => 'Financial Type',
374 'financial_type_id:name' => 'Machine name: Financial Type',
375 'payment_instrument_id:label' => 'Payment Method',
376 'payment_instrument_id:name' => 'Machine name: Payment Method',
377 ];
378 }
379
380 /**
381 * Get contributionRecur ID.
382 *
383 * @return int
384 */
385 protected function getContributionRecurID(): int {
386 if (!isset($this->contributionRecur)) {
387 $paymentProcessorID = $this->processorCreate();
388 $this->contributionRecur = $this->callAPISuccess('ContributionRecur', 'create', [
389 'contact_id' => $this->getContactID(),
390 'status_id' => 1,
391 'is_email_receipt' => 1,
392 'start_date' => '2021-07-23 15:39:20',
393 'end_date' => '2021-07-26 18:07:20',
394 'cancel_date' => '2021-08-19 09:12:45',
34795e7a 395 'next_sched_contribution_date' => '2021-09-08',
0f4031da
EM
396 'cancel_reason' => 'Because',
397 'amount' => 5990.99,
398 'currency' => 'EUR',
399 'frequency_unit' => 'year',
400 'frequency_interval' => 2,
401 'installments' => 24,
402 'payment_instrument_id' => 'Check',
403 'financial_type_id' => 'Member dues',
404 'processor_id' => 'abc',
405 'payment_processor_id' => $paymentProcessorID,
406 'trxn_id' => 123,
407 'invoice_id' => 'inv123',
408 'sequential' => 1,
409 'failure_retry_date' => '2020-01-03',
410 'auto_renew' => 1,
411 'cycle_day' => '15',
412 'is_test' => TRUE,
413 'payment_token_id' => $this->callAPISuccess('PaymentToken', 'create', [
414 'contact_id' => $this->getContactID(),
415 'token' => 456,
416 'payment_processor_id' => $paymentProcessorID,
417 ])['id'],
418 ])['values'][0];
419 }
420 return $this->contributionRecur['id'];
421 }
422
423 /**
424 * Get rendered output for contribution tokens.
425 *
426 * @return string
427 */
428 protected function getExpectedContributionRecurTokenOutPut(): string {
044c0ad1 429 return 'contribution_recur.id :' . $this->getContributionRecurID() . '
f70a513f 430contribution_recur.amount :€5,990.99
044c0ad1
EM
431contribution_recur.currency :EUR
432contribution_recur.frequency_unit :year
433contribution_recur.frequency_interval :2
434contribution_recur.installments :24
435contribution_recur.start_date :July 23rd, 2021 3:39 PM
436contribution_recur.create_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['create_date']) . '
437contribution_recur.modified_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['modified_date']) . '
438contribution_recur.cancel_date :August 19th, 2021 9:12 AM
439contribution_recur.cancel_reason :Because
440contribution_recur.end_date :July 26th, 2021 6:07 PM
441contribution_recur.processor_id :abc
442contribution_recur.payment_token_id :1
443contribution_recur.trxn_id :123
444contribution_recur.invoice_id :inv123
445contribution_recur.contribution_status_id :2
446contribution_recur.is_test:label :Yes
447contribution_recur.cycle_day :15
448contribution_recur.next_sched_contribution_date :September 8th, 2021
449contribution_recur.failure_count :0
450contribution_recur.failure_retry_date :January 3rd, 2020
451contribution_recur.auto_renew:label :Yes
452contribution_recur.payment_processor_id :1
453contribution_recur.financial_type_id :2
454contribution_recur.payment_instrument_id :4
455contribution_recur.is_email_receipt:label :Yes
456contribution_recur.frequency_unit:label :year
457contribution_recur.frequency_unit:name :year
458contribution_recur.contribution_status_id:label :Pending Label**
459contribution_recur.contribution_status_id:name :Pending
460contribution_recur.payment_processor_id:label :Dummy (test)
202723ae 461contribution_recur.payment_processor_id:name :Dummy_test
044c0ad1
EM
462contribution_recur.financial_type_id:label :Member Dues
463contribution_recur.financial_type_id:name :Member Dues
464contribution_recur.payment_instrument_id:label :Check
465contribution_recur.payment_instrument_id:name :Check
466';
467
0f4031da
EM
468 }
469
d41a5d53
EM
470 /**
471 * Test that membership tokens are consistently rendered.
472 *
473 * @throws \API_Exception
474 */
475 public function testMembershipTokenConsistency(): void {
476 $this->createLoggedInUser();
477 $this->restoreMembershipTypes();
478 $this->createCustomGroupWithFieldOfType(['extends' => 'Membership']);
479 $tokens = CRM_Core_SelectValues::membershipTokens();
d568dbe0
EM
480 $expectedTokens = $this->getMembershipTokens();
481 $this->assertEquals($expectedTokens, $tokens);
d41a5d53
EM
482 $newStyleTokens = "\n{membership.status_id:label}\n{membership.membership_type_id:label}\n";
483 $tokenString = $newStyleTokens . implode("\n", array_keys($this->getMembershipTokens()));
b024d6a1 484
d41a5d53
EM
485 $memberships = CRM_Utils_Token::getMembershipTokenDetails([$this->getMembershipID()]);
486 $messageToken = CRM_Utils_Token::getTokens($tokenString);
487 $tokenHtml = CRM_Utils_Token::replaceEntityTokens('membership', $memberships[$this->getMembershipID()], $tokenString, $messageToken);
488 $this->assertEquals($this->getExpectedMembershipTokenOutput(), $tokenHtml);
dd2f879a 489
b024d6a1
EM
490 // Custom fields work in the processor so test it....
491 $tokenString .= "\n{membership." . $this->getCustomFieldName('text') . '}';
dd2f879a
EM
492 // Now compare with scheduled reminder
493 $mut = new CiviMailUtils($this);
494 CRM_Utils_Time::setTime('2007-01-22 15:00:00');
495 $this->callAPISuccess('action_schedule', 'create', [
496 'title' => 'job',
497 'subject' => 'job',
498 'entity_value' => 1,
499 'mapping_id' => 4,
500 'start_action_date' => 'membership_join_date',
501 'start_action_offset' => 1,
502 'start_action_condition' => 'after',
503 'start_action_unit' => 'day',
504 'body_html' => $tokenString,
505 ]);
506 $this->callAPISuccess('job', 'send_reminder', []);
d568dbe0
EM
507 $expected = $this->getExpectedMembershipTokenOutput();
508 // Unlike the legacy method custom fields are resolved by the processor.
509 $expected .= "\nmy field";
510 $mut->checkMailLog([$expected]);
511
512 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
513 'controller' => __CLASS__,
514 'smarty' => FALSE,
515 'schema' => ['membershipId'],
516 ]);
517 $tokens = $tokenProcessor->listTokens();
518 // Add in custom tokens as token processor supports these.
889b0617 519 $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy());
3c78698e 520 $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens);
d568dbe0
EM
521 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
522 $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]);
523 $tokenProcessor->evaluate();
524 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
525
d41a5d53
EM
526 }
527
889b0617
EM
528 /**
529 * Get the advertised tokens the legacy function doesn't know about.
530 *
531 * @return string[]
532 */
533 public function getTokensAdvertisedByTokenProcessorButNotLegacy(): array {
534 return [
535 '{membership.custom_1}' => 'Enter text here :: Group with field text',
536 '{membership.source}' => 'Source',
537 '{membership.status_override_end_date}' => 'Status Override End Date',
538 ];
539 }
540
d41a5d53
EM
541 /**
542 * Get declared membership tokens.
543 *
544 * @return string[]
545 */
546 public function getMembershipTokens(): array {
547 return [
548 '{membership.id}' => 'Membership ID',
044c0ad1 549 '{membership.status_id:label}' => 'Status',
eac0a5bf 550 '{membership.membership_type_id:label}' => 'Membership Type',
d41a5d53 551 '{membership.start_date}' => 'Membership Start Date',
044c0ad1
EM
552 '{membership.join_date}' => 'Member Since',
553 '{membership.end_date}' => 'Membership Expiration Date',
d41a5d53
EM
554 '{membership.fee}' => 'Membership Fee',
555 ];
556 }
557
558 /**
559 * Get case ID.
560 *
561 * @return int
562 */
563 protected function getMembershipID(): int {
564 if (!isset($this->ids['Membership'][0])) {
b024d6a1
EM
565 $this->ids['Membership'][0] = $this->contactMembershipCreate([
566 'contact_id' => $this->getContactID(),
567 $this->getCustomFieldName('text') => 'my field',
568 ]);
d41a5d53
EM
569 }
570 return $this->ids['Membership'][0];
571 }
572
f399fbd8
EM
573 /**
574 * Get expected output from token parsing.
575 *
576 * @return string
577 */
e80f2261 578 protected function getExpectedParticipantTokenOutput(): string {
044c0ad1
EM
579 return 'participant.status_id :2
580participant.role_id :1
581participant.register_date :February 19th, 2007
582participant.source :Wimbeldon
583participant.fee_level :steep
f70a513f 584participant.fee_amount :$50.00
044c0ad1
EM
585participant.registered_by_id :
586participant.transferred_to_contact_id :
587participant.role_id:label :Attendee
588participant.balance :
589participant.custom_2 :99999
590participant.id :2
591participant.fee_currency :USD
592participant.discount_amount :
593participant.status_id:label :Attended
594participant.status_id:name :Attended
595participant.role_id:name :Attendee
596participant.is_test:label :No
597participant.must_wait :
e80f2261
EM
598';
599 }
600
601 /**
602 * Get expected output from token parsing.
603 *
604 * @return string
605 */
606 protected function getExpectedEventTokenOutput(): string {
044c0ad1
EM
607 return 'event.id :' . $this->ids['event'][0] . '
608event.title :Annual CiviCRM meet
609event.start_date :October 21st, 2008
610event.end_date :October 23rd, 2008
611event.event_type_id:label :Conference
612event.summary :If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
613event.contact_email :event@example.com
614event.contact_phone :456 789
615event.description :event description
616event.location :15 Walton St
f399fbd8
EM
617Emerald City, Maine 90210
618
044c0ad1
EM
619event.info_url :' . CRM_Utils_System::url('civicrm/event/info', NULL, TRUE) . '&reset=1&id=1
620event.registration_url :' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
621event.custom_1 :my field
622';
f399fbd8
EM
623 }
624
d41a5d53
EM
625 /**
626 * Get expected output from token parsing.
627 *
628 * @return string
629 */
630 protected function getExpectedMembershipTokenOutput(): string {
631 return '
632Expired
633General
6341
635Expired
636General
637January 21st, 2007
638January 21st, 2007
639December 21st, 2007
640100.00';
641 }
642
1ed50dc6
EM
643 /**
644 * Test that membership tokens are consistently rendered.
645 *
646 * @throws \API_Exception
647 */
648 public function testParticipantTokenConsistency(): void {
649 $this->createLoggedInUser();
e80f2261
EM
650 $this->setupParticipantScheduledReminder();
651
1ed50dc6 652 $tokens = CRM_Core_SelectValues::participantTokens();
889b0617 653 $this->assertEquals(array_diff_key($this->getParticipantTokens(), $this->getUnadvertisedTokens()), $tokens);
e80f2261
EM
654
655 $mut = new CiviMailUtils($this);
656
657 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
658 'controller' => __CLASS__,
659 'smarty' => FALSE,
660 'schema' => ['participantId'],
661 ]);
e3a34ebd 662 $this->assertEquals(array_merge($tokens, $this->getEventTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
e80f2261
EM
663
664 $this->callAPISuccess('job', 'send_reminder', []);
665 $expected = $this->getExpectedParticipantTokenOutput();
666 $mut->checkMailLog([$expected]);
667
044c0ad1 668 $tokenProcessor->addMessage('html', $this->getTokenString(array_keys($this->getParticipantTokens())), 'text/plain');
e80f2261
EM
669 $tokenProcessor->addRow(['participantId' => $this->ids['participant'][0]]);
670 $tokenProcessor->evaluate();
671 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
672
1ed50dc6
EM
673 }
674
2a7cae66
EM
675 /**
676 * Test that membership tokens are consistently rendered.
677 *
678 * @throws \API_Exception
679 * @throws \CRM_Core_Exception
680 */
681 public function testParticipantCustomDateToken(): void {
682 $this->createEventAndParticipant();
683 $dateFieldID = $this->createDateCustomField(['custom_group_id' => $this->ids['CustomGroup']['participant_'], 'default_value' => ''])['id'];
684 $input = '{participant.custom_' . $dateFieldID . '}';
685 $input .= '{participant.' . $this->getCustomFieldName('participant_int') . '}';
686 $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
687 'messageTemplate' => ['msg_html' => $input],
688 'tokenContext' => array_merge(['participantId' => $this->ids['participant'][0]], ['schema' => ['participantId', 'eventId']]),
689 ])['html'];
690 $this->assertEquals(99999, $tokenHtml);
691 }
692
1ed50dc6 693 /**
3c78698e 694 * Get declared participant tokens.
1ed50dc6
EM
695 *
696 * @return string[]
697 */
698 public function getParticipantTokens(): array {
699 return [
b7472bd6 700 '{participant.status_id}' => 'Status ID',
e80f2261 701 '{participant.role_id}' => 'Participant Role ID',
b7472bd6
EM
702 '{participant.register_date}' => 'Register date',
703 '{participant.source}' => 'Participant Source',
704 '{participant.fee_level}' => 'Fee level',
705 '{participant.fee_amount}' => 'Fee Amount',
706 '{participant.registered_by_id}' => 'Registered By Participant ID',
1ed50dc6 707 '{participant.transferred_to_contact_id}' => 'Transferred to Contact ID',
e80f2261
EM
708 '{participant.role_id:label}' => 'Participant Role',
709 '{participant.balance}' => 'Event Balance',
710 '{participant.' . $this->getCustomFieldName('participant_int') . '}' => 'Enter integer here :: participant_Group with field int',
711 '{participant.id}' => 'Participant ID',
712 '{participant.fee_currency}' => 'Fee Currency',
713 '{participant.discount_amount}' => 'Discount Amount',
714 '{participant.status_id:label}' => 'Status',
715 '{participant.status_id:name}' => 'Machine name: Status',
716 '{participant.role_id:name}' => 'Machine name: Participant Role',
717 '{participant.is_test:label}' => 'Test',
718 '{participant.must_wait}' => 'Must Wait on List',
1ed50dc6
EM
719 ];
720 }
721
3c78698e
EM
722 /**
723 * Test that domain tokens are consistently rendered.
724 */
725 public function testDomainTokenConsistency(): void {
726 $tokens = CRM_Core_SelectValues::domainTokens();
727 $this->assertEquals($this->getDomainTokens(), $tokens);
728 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
729 'controller' => __CLASS__,
730 'smarty' => FALSE,
731 ]);
732 $tokens['{domain.id}'] = 'Domain ID';
733 $tokens['{domain.description}'] = 'Domain Description';
dfe53edd 734 $tokens['{domain.now}'] = 'Current time/date';
3c78698e
EM
735 $this->assertEquals($tokens, $tokenProcessor->listTokens());
736 }
737
defba8ff
EM
738 /**
739 * @throws \API_Exception
740 * @throws \CRM_Core_Exception
741 */
742 public function testDomainNow(): void {
743 putenv('TIME_FUNC=frozen');
5e62af3d 744 CRM_Utils_Time::setTime('2021-09-18 23:58:00');
44dd64f0
EM
745 $modifiers = [
746 'shortdate' => '09/18/2021',
f85e1a4b 747 '%B %Y' => 'September 2021',
44dd64f0
EM
748 ];
749 foreach ($modifiers as $filter => $expected) {
750 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
751 'messageTemplate' => [
f85e1a4b 752 'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
44dd64f0
EM
753 ],
754 ])['text'];
f85e1a4b 755 $this->assertEquals($expected, $resolved);
44dd64f0 756 }
defba8ff
EM
757 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
758 'messageTemplate' => [
759 'msg_text' => '{domain.now}',
760 ],
761 ])['text'];
762 $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
f85e1a4b
TO
763
764 // This example is malformed - no quotes
765 try {
766 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
767 'messageTemplate' => [
768 'msg_text' => '{domain.now|crmDate:shortdate}',
769 ],
770 ])['text'];
e80f2261 771 $this->fail('Expected unquoted parameter to fail');
f85e1a4b
TO
772 }
773 catch (\CRM_Core_Exception $e) {
774 $this->assertRegExp(';Malformed token param;', $e->getMessage());
775 }
defba8ff
EM
776 }
777
3c78698e
EM
778 /**
779 * Get declared participant tokens.
780 *
781 * @return string[]
782 */
783 public function getDomainTokens(): array {
784 return [
785 '{domain.name}' => ts('Domain name'),
786 '{domain.address}' => ts('Domain (organization) address'),
787 '{domain.phone}' => ts('Domain (organization) phone'),
788 '{domain.email}' => 'Domain (organization) email',
789 '{domain.id}' => ts('Domain ID'),
790 '{domain.description}' => ts('Domain Description'),
defba8ff 791 '{domain.now}' => 'Current time/date',
b8fe55cd 792 '{domain.tax_term}' => 'Sales tax term (e.g VAT)',
3c78698e
EM
793 ];
794 }
795
ce971869 796 /**
e80f2261 797 * Test that event tokens are consistently rendered.
34795e7a
EM
798 *
799 * @throws \API_Exception
ce971869
EM
800 */
801 public function testEventTokenConsistency(): void {
f399fbd8
EM
802 $mut = new CiviMailUtils($this);
803 $this->setupParticipantScheduledReminder();
804
ce971869 805 $tokens = CRM_Core_SelectValues::eventTokens();
889b0617 806 $this->assertEquals(array_merge($this->getEventTokens()), $tokens);
ce971869
EM
807 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
808 'controller' => __CLASS__,
809 'smarty' => FALSE,
810 'schema' => ['eventId'],
811 ]);
812 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
f399fbd8 813
044c0ad1 814 $expectedEventString = $this->getExpectedEventTokenOutput();
e80f2261 815 $this->callAPISuccess('job', 'send_reminder', []);
044c0ad1
EM
816 $expectedParticipantString = $this->getExpectedParticipantTokenOutput();
817 $toCheck = array_merge(explode("\n", $expectedEventString), explode("\n", $expectedParticipantString));
818 $toCheck[] = $expectedEventString;
819 $toCheck[] = $expectedParticipantString;
820 $mut->checkMailLog($toCheck);
821 $tokens = array_keys($this->getEventTokens());
822 $html = $this->getTokenString($tokens);
823 $tokenProcessor->addMessage('html', $html, 'text/plain');
e80f2261
EM
824 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
825 $tokenProcessor->evaluate();
044c0ad1 826 $this->assertEquals($expectedEventString, $tokenProcessor->getRow(0)->render('html'));
e80f2261
EM
827 }
828
829 /**
830 * Test that event tokens work absent participant tokens.
831 *
832 * @throws \API_Exception
833 */
834 public function testEventTokenConsistencyNoParticipantTokens(): void {
835 $mut = new CiviMailUtils($this);
836 $this->setupParticipantScheduledReminder(FALSE);
837
f399fbd8
EM
838 $this->callAPISuccess('job', 'send_reminder', []);
839 $expected = $this->getExpectedEventTokenOutput();
044c0ad1
EM
840 // Checking these individually is easier to decipher discrepancies
841 // but we also want to check in entirety.
842 $toCheck = explode("\n", $expected);
843 $toCheck[] = $expected;
844 $mut->checkMailLog($toCheck);
e80f2261
EM
845
846 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
847 'controller' => __CLASS__,
848 'smarty' => FALSE,
849 'schema' => ['eventId'],
850 ]);
044c0ad1 851 $html = $this->getTokenString(array_keys($this->getEventTokens()));
e80f2261 852
044c0ad1 853 $tokenProcessor->addMessage('html', $html, 'text/plain');
e80f2261
EM
854 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
855 $tokenProcessor->evaluate();
856 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
857
f399fbd8
EM
858 }
859
860 /**
861 * Set up scheduled reminder for participants.
862 *
863 * @throws \API_Exception
864 */
e80f2261 865 public function setupParticipantScheduledReminder($includeParticipant = TRUE): void {
2a7cae66 866 $this->createEventAndParticipant();
044c0ad1 867 $tokens = array_keys($this->getEventTokens());
e80f2261 868 if ($includeParticipant) {
044c0ad1 869 $tokens = array_keys(array_merge($this->getEventTokens(), $this->getParticipantTokens()));
e80f2261 870 }
044c0ad1 871 $html = $this->getTokenString($tokens);
f399fbd8
EM
872 CRM_Utils_Time::setTime('2007-02-20 15:00:00');
873 $this->callAPISuccess('action_schedule', 'create', [
874 'title' => 'job',
875 'subject' => 'job',
876 'entity_value' => 1,
877 'mapping_id' => 2,
878 'start_action_date' => 'register_date',
879 'start_action_offset' => 1,
880 'start_action_condition' => 'after',
881 'start_action_unit' => 'day',
e80f2261 882 'body_html' => $html,
f399fbd8 883 ]);
ce971869
EM
884 }
885
886 /**
887 * Get expected event tokens.
888 *
889 * @return string[]
890 */
891 protected function getEventTokens(): array {
892 return [
e80f2261 893 '{event.id}' => 'Event ID',
ce971869
EM
894 '{event.title}' => 'Event Title',
895 '{event.start_date}' => 'Event Start Date',
896 '{event.end_date}' => 'Event End Date',
e80f2261 897 '{event.event_type_id:label}' => 'Event Type',
ce971869
EM
898 '{event.summary}' => 'Event Summary',
899 '{event.contact_email}' => 'Event Contact Email',
900 '{event.contact_phone}' => 'Event Contact Phone',
901 '{event.description}' => 'Event Description',
902 '{event.location}' => 'Event Location',
ce971869
EM
903 '{event.info_url}' => 'Event Info URL',
904 '{event.registration_url}' => 'Event Registration URL',
f399fbd8 905 '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
ce971869
EM
906 ];
907 }
908
044c0ad1
EM
909 /**
910 * @param array $tokens
911 *
912 * @return string
913 */
914 protected function getTokenString(array $tokens): string {
915 $html = '';
916 foreach ($tokens as $token) {
917 $html .= substr($token, 1, -1) . ' :' . $token . "\n";
918 }
919 return $html;
920 }
921
2a7cae66
EM
922 /**
923 * Create an event with a participant.
924 *
925 * @throws \API_Exception
926 */
927 protected function createEventAndParticipant(): void {
928 $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
929 $this->createCustomGroupWithFieldOfType(['extends' => 'Participant'], 'int', 'participant_');
930 $emailID = Email::create()
931 ->setValues(['email' => 'event@example.com'])
932 ->execute()
933 ->first()['id'];
934 $addressID = Address::create()->setValues([
935 'street_address' => '15 Walton St',
936 'supplemental_address_1' => 'up the road',
937 'city' => 'Emerald City',
938 'state_province_id:label' => 'Maine',
939 'postal_code' => 90210,
940 ])->execute()->first()['id'];
941 $phoneID = Phone::create()
942 ->setValues(['phone' => '456 789'])
943 ->execute()
944 ->first()['id'];
945
946 $locationBlockID = LocBlock::save(FALSE)->setRecords([
947 [
948 'email_id' => $emailID,
949 'address_id' => $addressID,
950 'phone_id' => $phoneID,
951 ],
952 ])->execute()->first()['id'];
953 $this->ids['event'][0] = $this->eventCreate([
954 'description' => 'event description',
955 $this->getCustomFieldName('text') => 'my field',
956 'loc_block_id' => $locationBlockID,
957 ])['id'];
958 // Create an unrelated participant record so that the ids don't match.
959 // this prevents things working just because the id 'happens to be valid'
960 $this->participantCreate([
961 'register_date' => '2020-01-01',
962 'event_id' => $this->ids['event'][0],
963 ]);
964 $this->ids['participant'][0] = $this->participantCreate([
965 'event_id' => $this->ids['event'][0],
966 'fee_amount' => 50,
967 'fee_level' => 'steep',
968 $this->getCustomFieldName('participant_int') => '99999',
969 ]);
970 }
971
b05a7c72 972 public function testEscaping() {
167f848f
TO
973 $autoClean = [];
974 $create = function(string $entity, array $record = []) use (&$autoClean) {
975 // It's convenient to use createTestObject(), but it doesn't reproduce the normal escaping rules from QuickForm/APIv3/APIv4.
b05a7c72 976 CRM_Utils_API_HTMLInputCoder::singleton()->encodeRow($record);
167f848f
TO
977 $dao = CRM_Core_DAO::createTestObject(CRM_Core_DAO_AllCoreTables::getFullName($entity), $record);
978
979 // We're not using transactions, and truncating 'contact' seems problematic, so we roll up our sleeves and cleanup each record...
980 $autoClean[] = CRM_Utils_AutoClean::with(function() use ($entity, $dao) {
981 CRM_Core_DAO::deleteTestObjects(CRM_Core_DAO_AllCoreTables::getFullName($entity), ['id' => $dao->id]);
982 });
983
984 return $dao;
b05a7c72
TO
985 };
986
987 $context = [];
988 $context['contactId'] = $create('Contact', [
989 'first_name' => '<b>ig</b>illy brackets',
990 ])->id;
991 $context['eventId'] = $create('Event', [
992 'title' => 'The Webinar',
993 'description' => '<p>Some online webinar thingy.</p> <p>Attendees will need to install the <a href="http://telefoo.example.com">TeleFoo</a> app.</p>',
994 ])->id;
995
996 $messages = $expected = [];
997
998 // The `first_name` does not allow HTML. Any funny characters are presented like literal text.
999 $messages['contact_text'] = 'Hello {contact.first_name}!';
b9466f09 1000 $expected['contact_text'] = 'Hello <b>ig</b>illy brackets!';
b05a7c72 1001
b9466f09
EM
1002 $messages['contact_html'] = '<p>Hello {contact.first_name}!</p>';
1003 $expected['contact_html'] = '<p>Hello &lt;b&gt;ig&lt;/b&gt;illy brackets!</p>';
b05a7c72
TO
1004
1005 // The `description` does allow HTML. Any funny characters are filtered out of text.
1006 $messages['event_text'] = 'You signed up for this event: {event.title}: {event.description}';
1007 $expected['event_text'] = 'You signed up for this event: The Webinar: Some online webinar thingy. Attendees will need to install the TeleFoo app.';
1008
b9466f09 1009 $messages['event_html'] = '<p>You signed up for this event:</p> <h3>{event.title}</h3> {event.description}';
b05a7c72
TO
1010 $expected['event_html'] = '<p>You signed up for this event:</p> <h3>The Webinar</h3> <p>Some online webinar thingy.</p> <p>Attendees will need to install the <a href="http://telefoo.example.com">TeleFoo</a> app.</p>';
1011
1012 $rendered = CRM_Core_TokenSmarty::render($messages, $context);
1013
1014 $this->assertEquals($expected, $rendered);
1015 }
1016
d0ce76fd 1017}