Test & align empty token handling
[civicrm-core.git] / tests / phpunit / CRM / Utils / TokenConsistencyTest.php
index 34d5fea5febaa6f4cd9b917d0523ee8c096fd4bb..7e7f00c1f14beb690744372264e0320914d4d179 100644 (file)
  */
 
 use Civi\Token\TokenProcessor;
+use Civi\Api4\LocBlock;
+use Civi\Api4\Email;
+use Civi\Api4\Phone;
+use Civi\Api4\Address;
 
 /**
  * CRM_Utils_TokenConsistencyTest
@@ -40,12 +44,9 @@ class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
 
   /**
    * Post test cleanup.
-   *
-   * @throws \API_Exception
-   * @throws \CRM_Core_Exception
    */
   public function tearDown(): void {
-    $this->quickCleanup(['civicrm_case', 'civicrm_case_type'], TRUE);
+    $this->quickCleanup(['civicrm_case', 'civicrm_case_type', 'civicrm_participant', 'civicrm_event'], TRUE);
     parent::tearDown();
   }
 
@@ -61,7 +62,7 @@ class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
     $tokens = CRM_Core_SelectValues::caseTokens();
     $this->assertEquals($this->getCaseTokens(), $tokens);
     $caseID = $this->getCaseID();
-    $tokenString = implode("\n", array_keys($this->getCaseTokens()));
+    $tokenString = $this->getTokenString(array_keys($this->getCaseTokens()));
     $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString, ['case' => $this->getCaseTokenKeys()]);
     $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
     // Now do the same without passing in 'knownTokens'
@@ -71,21 +72,12 @@ class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
     // And check our deprecated tokens still work.
     $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, '{case.case_type_id} {case.status_id}');
     $this->assertEquals('Housing Support Ongoing', $tokenHtml);
-
-    $additionalTokensFromProcessor = [
-      '{case.case_type_id}' => 'Case Type ID',
-      '{case.status_id}' => 'Case Status',
-      '{case.case_type_id:name}' => 'Machine name: Case Type',
-      '{case.status_id:name}' => 'Machine name: Case Status',
-    ];
-    $expectedTokens = array_merge($this->getCaseTokens(), $additionalTokensFromProcessor);
-
     $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
       'controller' => __CLASS__,
       'smarty' => FALSE,
       'schema' => ['caseId'],
     ]);
-    $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
+    $this->assertEquals(array_merge($this->getCaseTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
     $tokenProcessor->addRow([
       'caseId' => $this->getCaseID(),
     ]);
@@ -104,16 +96,17 @@ class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
    * @return string
    */
   protected function getExpectedCaseTokenOutput(): string {
-    return '1
-Housing Support
-Case Subject
-July 23rd, 2021
-July 26th, 2021
-case details
-Ongoing
-No
-' . CRM_Utils_Date::customFormat($this->case['created_date']) . '
-' . CRM_Utils_Date::customFormat($this->case['modified_date']) . '
+    return 'case.id :1
+case.case_type_id:label :Housing Support
+case.subject :Case Subject
+case.start_date :July 23rd, 2021
+case.end_date :July 26th, 2021
+case.details :case details
+case.status_id:label :Ongoing
+case.is_deleted:label :No
+case.created_date :' . CRM_Utils_Date::customFormat($this->case['created_date']) . '
+case.modified_date :' . CRM_Utils_Date::customFormat($this->case['modified_date']) . '
+case.custom_1 :' . '
 ';
   }
 
@@ -203,8 +196,9 @@ No
       'smarty' => FALSE,
       'schema' => ['contribution_recurId'],
     ]);
-    $this->assertEquals(array_merge($this->getContributionRecurTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
-    $tokenString = implode("\n", array_keys($this->getContributionRecurTokens()));
+    $expectedTokens = array_merge($this->getContributionRecurTokens(), $this->getDomainTokens());
+    $this->assertEquals(array_diff_key($expectedTokens, $this->getUnadvertisedTokens()), $tokenProcessor->listTokens());
+    $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens()));
 
     $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
     $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
@@ -212,6 +206,73 @@ No
     $this->assertEquals($this->getExpectedContributionRecurTokenOutPut(), $tokenProcessor->getRow(0)->render('html'));
   }
 
+  /**
+   * Get tokens that are not advertised via listTokens.
+   *
+   * @return string[]
+   */
+  public function getUnadvertisedTokens(): array {
+    return [
+      '{membership.status_id}' => 'Status ID',
+      '{membership.membership_type_id}' => 'Membership Type ID',
+      '{membership.status_id:name}' => 'Machine name: Status',
+      '{membership.membership_type_id:name}' => 'Machine name: Membership Type',
+      '{contribution_recur.frequency_unit}' => 'Frequency Unit',
+      '{contribution_recur.contribution_status_id}' => 'Status',
+      '{contribution_recur.payment_processor_id}' => 'Payment Processor ID',
+      '{contribution_recur.financial_type_id}' => 'Financial Type ID',
+      '{contribution_recur.payment_instrument_id}' => 'Payment Method',
+      '{contribution_recur.frequency_unit:name}' => 'Machine name: Frequency Unit',
+      '{contribution_recur.payment_instrument_id:name}' => 'Machine name: Payment Method',
+      '{contribution_recur.contribution_status_id:name}' => 'Machine name: Status',
+      '{contribution_recur.payment_processor_id:name}' => 'Machine name: Payment Processor',
+      '{contribution_recur.financial_type_id:name}' => 'Machine name: Financial Type',
+      '{participant.status_id:name}' => 'Machine name: Status',
+      '{participant.role_id:name}' => 'Machine name: Participant Role',
+      '{participant.status_id}' => 'Status ID',
+      '{participant.role_id}' => 'Participant Role ID',
+    ];
+  }
+
+  /**
+   * Test tokens in 2 ways to ensure consistent handling.
+   *
+   * 1) as part of the greeting processing
+   * 2) via the token processor.
+   *
+   */
+  public function testOddTokens(): void {
+
+    $variants = [
+      [
+        'string' => '{contact.individual_prefix}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.individual_suffix}',
+        'expected' => 'Mr. Anthony  Anderson II',
+      ],
+      [
+        'string' => '{contact.prefix_id:label}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.suffix_id:label}',
+        'expected' => 'Mr. Anthony  Anderson II',
+      ],
+    ];
+    $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
+      'smarty' => FALSE,
+      'schema' => ['contactId'],
+    ]);
+    $contactID = $this->individualCreate(['middle_name' => '']);
+    $tokenProcessor->addRow(['contactId' => $contactID]);
+    $tokenProcessor->evaluate();
+    foreach ($variants as $index => $variant) {
+      $tokenProcessor->addMessage($index, $variant['string'], 'text/plain');
+    }
+    $tokenProcessor->evaluate();
+    $result = $tokenProcessor->getRow(0);
+    foreach ($variants as $index => $variant) {
+      $greetingString = $variant['string'];
+      CRM_Utils_Token::replaceGreetingTokens($greetingString, $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]), $contactID);
+      $this->assertEquals($variant['expected'], $greetingString);
+      $this->assertEquals($variant['expected'], $result->render($index));
+    }
+  }
+
   /**
    * Get the contribution recur tokens keyed by the token.
    *
@@ -284,6 +345,7 @@ No
         'start_date' => '2021-07-23 15:39:20',
         'end_date' => '2021-07-26 18:07:20',
         'cancel_date' => '2021-08-19 09:12:45',
+        'next_sched_contribution_date' => '2021-09-08',
         'cancel_reason' => 'Because',
         'amount' => 5990.99,
         'currency' => 'EUR',
@@ -317,43 +379,45 @@ No
    * @return string
    */
   protected function getExpectedContributionRecurTokenOutPut(): string {
-    return $this->getContributionRecurID() . '
-€ 5,990.99
-EUR
-year
-2
-24
-July 23rd, 2021  3:39 PM
-' . CRM_Utils_Date::customFormat($this->contributionRecur['create_date']) . '
-' . CRM_Utils_Date::customFormat($this->contributionRecur['modified_date']) . '
-August 19th, 2021  9:12 AM
-Because
-July 26th, 2021  6:07 PM
-abc
-1
-123
-inv123
-2
-Yes
-15
-
-0
-January 3rd, 2020 12:00 AM
-Yes
-1
-2
-4
-Yes
-year
-year
-Pending Label**
-Pending
-Dummy (test)
-Dummy (test)
-Member Dues
-Member Dues
-Check
-Check';
+    return 'contribution_recur.id :' . $this->getContributionRecurID() . '
+contribution_recur.amount :€ 5,990.99
+contribution_recur.currency :EUR
+contribution_recur.frequency_unit :year
+contribution_recur.frequency_interval :2
+contribution_recur.installments :24
+contribution_recur.start_date :July 23rd, 2021  3:39 PM
+contribution_recur.create_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['create_date']) . '
+contribution_recur.modified_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['modified_date']) . '
+contribution_recur.cancel_date :August 19th, 2021  9:12 AM
+contribution_recur.cancel_reason :Because
+contribution_recur.end_date :July 26th, 2021  6:07 PM
+contribution_recur.processor_id :abc
+contribution_recur.payment_token_id :1
+contribution_recur.trxn_id :123
+contribution_recur.invoice_id :inv123
+contribution_recur.contribution_status_id :2
+contribution_recur.is_test:label :Yes
+contribution_recur.cycle_day :15
+contribution_recur.next_sched_contribution_date :September 8th, 2021
+contribution_recur.failure_count :0
+contribution_recur.failure_retry_date :January 3rd, 2020
+contribution_recur.auto_renew:label :Yes
+contribution_recur.payment_processor_id :1
+contribution_recur.financial_type_id :2
+contribution_recur.payment_instrument_id :4
+contribution_recur.is_email_receipt:label :Yes
+contribution_recur.frequency_unit:label :year
+contribution_recur.frequency_unit:name :year
+contribution_recur.contribution_status_id:label :Pending Label**
+contribution_recur.contribution_status_id:name :Pending
+contribution_recur.payment_processor_id:label :Dummy (test)
+contribution_recur.payment_processor_id:name :Dummy (test)
+contribution_recur.financial_type_id:label :Member Dues
+contribution_recur.financial_type_id:name :Member Dues
+contribution_recur.payment_instrument_id:label :Check
+contribution_recur.payment_instrument_id:name :Check
+';
+
   }
 
   /**
@@ -405,7 +469,7 @@ Check';
     ]);
     $tokens = $tokenProcessor->listTokens();
     // Add in custom tokens as token processor supports these.
-    $expectedTokens['{membership.custom_1}'] = 'Enter text here :: Group with field text';
+    $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy());
     $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens);
     $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
     $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]);
@@ -414,6 +478,19 @@ Check';
 
   }
 
+  /**
+   * Get the advertised tokens the legacy function doesn't know about.
+   *
+   * @return string[]
+   */
+  public function getTokensAdvertisedByTokenProcessorButNotLegacy(): array {
+    return [
+      '{membership.custom_1}' => 'Enter text here :: Group with field text',
+      '{membership.source}' => 'Source',
+      '{membership.status_override_end_date}' => 'Status Override End Date',
+    ];
+  }
+
   /**
    * Get declared membership tokens.
    *
@@ -422,11 +499,11 @@ Check';
   public function getMembershipTokens(): array {
     return [
       '{membership.id}' => 'Membership ID',
-      '{membership.status_id:label}' => 'Membership Status',
+      '{membership.status_id:label}' => 'Status',
       '{membership.membership_type_id:label}' => 'Membership Type',
       '{membership.start_date}' => 'Membership Start Date',
-      '{membership.join_date}' => 'Membership Join Date',
-      '{membership.end_date}' => 'Membership End Date',
+      '{membership.join_date}' => 'Member Since',
+      '{membership.end_date}' => 'Membership Expiration Date',
       '{membership.fee}' => 'Membership Fee',
     ];
   }
@@ -446,6 +523,58 @@ Check';
     return $this->ids['Membership'][0];
   }
 
+  /**
+   * Get expected output from token parsing.
+   *
+   * @return string
+   */
+  protected function getExpectedParticipantTokenOutput(): string {
+    return 'participant.status_id :2
+participant.role_id :1
+participant.register_date :February 19th, 2007
+participant.source :Wimbeldon
+participant.fee_level :steep
+participant.fee_amount :$ 50.00
+participant.registered_by_id :
+participant.transferred_to_contact_id :
+participant.role_id:label :Attendee
+participant.balance :
+participant.custom_2 :99999
+participant.id :2
+participant.fee_currency :USD
+participant.discount_amount :
+participant.status_id:label :Attended
+participant.status_id:name :Attended
+participant.role_id:name :Attendee
+participant.is_test:label :No
+participant.must_wait :
+';
+  }
+
+  /**
+   * Get expected output from token parsing.
+   *
+   * @return string
+   */
+  protected function getExpectedEventTokenOutput(): string {
+    return 'event.id :' . $this->ids['event'][0] . '
+event.title :Annual CiviCRM meet
+event.start_date :October 21st, 2008
+event.end_date :October 23rd, 2008
+event.event_type_id:label :Conference
+event.summary :If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
+event.contact_email :event@example.com
+event.contact_phone :456 789
+event.description :event description
+event.location :15 Walton St
+Emerald City, Maine 90210
+
+event.info_url :' . CRM_Utils_System::url('civicrm/event/info', NULL, TRUE) . '&reset=1&id=1
+event.registration_url :' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
+event.custom_1 :my field
+';
+  }
+
   /**
    * Get expected output from token parsing.
    *
@@ -471,9 +600,47 @@ December 21st, 2007
    */
   public function testParticipantTokenConsistency(): void {
     $this->createLoggedInUser();
-    $this->createCustomGroupWithFieldOfType(['extends' => 'Participant']);
+    $this->setupParticipantScheduledReminder();
+
     $tokens = CRM_Core_SelectValues::participantTokens();
-    $this->assertEquals($this->getParticipantTokens(), $tokens);
+    $this->assertEquals(array_diff_key($this->getParticipantTokens(), $this->getUnadvertisedTokens()), $tokens);
+
+    $mut = new CiviMailUtils($this);
+
+    $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
+      'controller' => __CLASS__,
+      'smarty' => FALSE,
+      'schema' => ['participantId'],
+    ]);
+    $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
+
+    $this->callAPISuccess('job', 'send_reminder', []);
+    $expected = $this->getExpectedParticipantTokenOutput();
+    $mut->checkMailLog([$expected]);
+
+    $tokenProcessor->addMessage('html', $this->getTokenString(array_keys($this->getParticipantTokens())), 'text/plain');
+    $tokenProcessor->addRow(['participantId' => $this->ids['participant'][0]]);
+    $tokenProcessor->evaluate();
+    $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
+
+  }
+
+  /**
+   * Test that membership tokens are consistently rendered.
+   *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   */
+  public function testParticipantCustomDateToken(): void {
+    $this->createEventAndParticipant();
+    $dateFieldID = $this->createDateCustomField(['custom_group_id' => $this->ids['CustomGroup']['participant_'], 'default_value' => ''])['id'];
+    $input = '{participant.custom_' . $dateFieldID . '}';
+    $input .= '{participant.' . $this->getCustomFieldName('participant_int') . '}';
+    $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
+      'messageTemplate' => ['msg_html' => $input],
+      'tokenContext' => array_merge(['participantId' => $this->ids['participant'][0]], ['schema' => ['participantId', 'eventId']]),
+    ])['html'];
+    $this->assertEquals(99999, $tokenHtml);
   }
 
   /**
@@ -484,16 +651,24 @@ December 21st, 2007
   public function getParticipantTokens(): array {
     return [
       '{participant.status_id}' => 'Status ID',
-      '{participant.role_id}' => 'Participant Role (ID)',
+      '{participant.role_id}' => 'Participant Role ID',
       '{participant.register_date}' => 'Register date',
       '{participant.source}' => 'Participant Source',
       '{participant.fee_level}' => 'Fee level',
       '{participant.fee_amount}' => 'Fee Amount',
       '{participant.registered_by_id}' => 'Registered By Participant ID',
       '{participant.transferred_to_contact_id}' => 'Transferred to Contact ID',
-      '{participant.role_id:label}' => 'Participant Role (label)',
-      '{participant.fee_label}' => 'Fee Label',
-      '{participant.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
+      '{participant.role_id:label}' => 'Participant Role',
+      '{participant.balance}' => 'Event Balance',
+      '{participant.' . $this->getCustomFieldName('participant_int') . '}' => 'Enter integer here :: participant_Group with field int',
+      '{participant.id}' => 'Participant ID',
+      '{participant.fee_currency}' => 'Fee Currency',
+      '{participant.discount_amount}' => 'Discount Amount',
+      '{participant.status_id:label}' => 'Status',
+      '{participant.status_id:name}' => 'Machine name: Status',
+      '{participant.role_id:name}' => 'Machine name: Participant Role',
+      '{participant.is_test:label}' => 'Test',
+      '{participant.must_wait}' => 'Must Wait on List',
     ];
   }
 
@@ -509,7 +684,7 @@ December 21st, 2007
     ]);
     $tokens['{domain.id}'] = 'Domain ID';
     $tokens['{domain.description}'] = 'Domain Description';
-    $tokens['domain.now'] = 'Current time/date';
+    $tokens['{domain.now}'] = 'Current time/date';
     $this->assertEquals($tokens, $tokenProcessor->listTokens());
   }
 
@@ -519,13 +694,19 @@ December 21st, 2007
    */
   public function testDomainNow(): void {
     putenv('TIME_FUNC=frozen');
-    CRM_Utils_Time::setTime('2021-21-18 11:58:00');
-    $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
-      'messageTemplate' => [
-        'msg_text' => '{domain.now|crmDate:short}',
-      ],
-    ])['text'];
-    $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
+    CRM_Utils_Time::setTime('2021-09-18 23:58:00');
+    $modifiers = [
+      'shortdate' => '09/18/2021',
+      '%B %Y' => 'September 2021',
+    ];
+    foreach ($modifiers as $filter => $expected) {
+      $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
+        'messageTemplate' => [
+          'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
+        ],
+      ])['text'];
+      $this->assertEquals($expected, $resolved);
+    }
     $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
       'messageTemplate' => [
         'msg_text' => '{domain.now}',
@@ -533,7 +714,18 @@ December 21st, 2007
     ])['text'];
     $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
 
-    $b1 = 1;
+    // This example is malformed - no quotes
+    try {
+      $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
+        'messageTemplate' => [
+          'msg_text' => '{domain.now|crmDate:shortdate}',
+        ],
+      ])['text'];
+      $this->fail('Expected unquoted parameter to fail');
+    }
+    catch (\CRM_Core_Exception $e) {
+      $this->assertRegExp(';Malformed token param;', $e->getMessage());
+    }
   }
 
   /**
@@ -554,17 +746,93 @@ December 21st, 2007
   }
 
   /**
-   * Test that domain tokens are consistently rendered.
+   * Test that event tokens are consistently rendered.
+   *
+   * @throws \API_Exception
    */
   public function testEventTokenConsistency(): void {
+    $mut = new CiviMailUtils($this);
+    $this->setupParticipantScheduledReminder();
+
     $tokens = CRM_Core_SelectValues::eventTokens();
-    $this->assertEquals($this->getEventTokens(), $tokens);
+    $this->assertEquals(array_merge($this->getEventTokens()), $tokens);
     $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
       'controller' => __CLASS__,
       'smarty' => FALSE,
       'schema' => ['eventId'],
     ]);
     $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
+
+    $expectedEventString = $this->getExpectedEventTokenOutput();
+    $this->callAPISuccess('job', 'send_reminder', []);
+    $expectedParticipantString = $this->getExpectedParticipantTokenOutput();
+    $toCheck = array_merge(explode("\n", $expectedEventString), explode("\n", $expectedParticipantString));
+    $toCheck[] = $expectedEventString;
+    $toCheck[] = $expectedParticipantString;
+    $mut->checkMailLog($toCheck);
+    $tokens = array_keys($this->getEventTokens());
+    $html = $this->getTokenString($tokens);
+    $tokenProcessor->addMessage('html', $html, 'text/plain');
+    $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
+    $tokenProcessor->evaluate();
+    $this->assertEquals($expectedEventString, $tokenProcessor->getRow(0)->render('html'));
+  }
+
+  /**
+   * Test that event tokens work absent participant tokens.
+   *
+   * @throws \API_Exception
+   */
+  public function testEventTokenConsistencyNoParticipantTokens(): void {
+    $mut = new CiviMailUtils($this);
+    $this->setupParticipantScheduledReminder(FALSE);
+
+    $this->callAPISuccess('job', 'send_reminder', []);
+    $expected = $this->getExpectedEventTokenOutput();
+    // Checking these individually is easier to decipher discrepancies
+    // but we also want to check in entirety.
+    $toCheck = explode("\n", $expected);
+    $toCheck[] = $expected;
+    $mut->checkMailLog($toCheck);
+
+    $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
+      'controller' => __CLASS__,
+      'smarty' => FALSE,
+      'schema' => ['eventId'],
+    ]);
+    $html = $this->getTokenString(array_keys($this->getEventTokens()));
+
+    $tokenProcessor->addMessage('html', $html, 'text/plain');
+    $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
+    $tokenProcessor->evaluate();
+    $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
+
+  }
+
+  /**
+   * Set up scheduled reminder for participants.
+   *
+   * @throws \API_Exception
+   */
+  public function setupParticipantScheduledReminder($includeParticipant = TRUE): void {
+    $this->createEventAndParticipant();
+    $tokens = array_keys($this->getEventTokens());
+    if ($includeParticipant) {
+      $tokens = array_keys(array_merge($this->getEventTokens(), $this->getParticipantTokens()));
+    }
+    $html = $this->getTokenString($tokens);
+    CRM_Utils_Time::setTime('2007-02-20 15:00:00');
+    $this->callAPISuccess('action_schedule', 'create', [
+      'title' => 'job',
+      'subject' => 'job',
+      'entity_value' => 1,
+      'mapping_id' => 2,
+      'start_action_date' => 'register_date',
+      'start_action_offset' => 1,
+      'start_action_condition' => 'after',
+      'start_action_unit' => 'day',
+      'body_html' => $html,
+    ]);
   }
 
   /**
@@ -574,21 +842,83 @@ December 21st, 2007
    */
   protected function getEventTokens(): array {
     return [
-      '{event.event_id}' => 'Event ID',
+      '{event.id}' => 'Event ID',
       '{event.title}' => 'Event Title',
       '{event.start_date}' => 'Event Start Date',
       '{event.end_date}' => 'Event End Date',
-      '{event.event_type}' => 'Event Type',
+      '{event.event_type_id:label}' => 'Event Type',
       '{event.summary}' => 'Event Summary',
       '{event.contact_email}' => 'Event Contact Email',
       '{event.contact_phone}' => 'Event Contact Phone',
       '{event.description}' => 'Event Description',
       '{event.location}' => 'Event Location',
-      '{event.fee_amount}' => 'Event Fee',
       '{event.info_url}' => 'Event Info URL',
       '{event.registration_url}' => 'Event Registration URL',
-      '{event.balance}' => 'Event Balance',
+      '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
     ];
   }
 
+  /**
+   * @param array $tokens
+   *
+   * @return string
+   */
+  protected function getTokenString(array $tokens): string {
+    $html = '';
+    foreach ($tokens as $token) {
+      $html .= substr($token, 1, -1) . ' :' . $token . "\n";
+    }
+    return $html;
+  }
+
+  /**
+   * Create an event with a participant.
+   *
+   * @throws \API_Exception
+   */
+  protected function createEventAndParticipant(): void {
+    $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
+    $this->createCustomGroupWithFieldOfType(['extends' => 'Participant'], 'int', 'participant_');
+    $emailID = Email::create()
+      ->setValues(['email' => 'event@example.com'])
+      ->execute()
+      ->first()['id'];
+    $addressID = Address::create()->setValues([
+      'street_address' => '15 Walton St',
+      'supplemental_address_1' => 'up the road',
+      'city' => 'Emerald City',
+      'state_province_id:label' => 'Maine',
+      'postal_code' => 90210,
+    ])->execute()->first()['id'];
+    $phoneID = Phone::create()
+      ->setValues(['phone' => '456 789'])
+      ->execute()
+      ->first()['id'];
+
+    $locationBlockID = LocBlock::save(FALSE)->setRecords([
+      [
+        'email_id' => $emailID,
+        'address_id' => $addressID,
+        'phone_id' => $phoneID,
+      ],
+    ])->execute()->first()['id'];
+    $this->ids['event'][0] = $this->eventCreate([
+      'description' => 'event description',
+      $this->getCustomFieldName('text') => 'my field',
+      'loc_block_id' => $locationBlockID,
+    ])['id'];
+    // Create an unrelated participant record so that the ids don't match.
+    // this prevents things working just because the id 'happens to be valid'
+    $this->participantCreate([
+      'register_date' => '2020-01-01',
+      'event_id' => $this->ids['event'][0],
+    ]);
+    $this->ids['participant'][0] = $this->participantCreate([
+      'event_id' => $this->ids['event'][0],
+      'fee_amount' => 50,
+      'fee_level' => 'steep',
+      $this->getCustomFieldName('participant_int') => '99999',
+    ]);
+  }
+
 }