CaseActivity - Define class-model and unit-test for workflow message. Add base test...
authorTim Otten <totten@civicrm.org>
Tue, 6 Jul 2021 06:32:29 +0000 (23:32 -0700)
committerTim Otten <totten@civicrm.org>
Mon, 13 Sep 2021 22:33:59 +0000 (15:33 -0700)
CRM/Case/WorkflowMessage/CaseActivity.php [new file with mode: 0644]
CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php [new file with mode: 0644]
CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php [new file with mode: 0644]
Civi/Test/WorkflowMessageTestTrait.php [new file with mode: 0644]
Civi/Token/TokenCompatSubscriber.php
tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php [new file with mode: 0644]
tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php

diff --git a/CRM/Case/WorkflowMessage/CaseActivity.php b/CRM/Case/WorkflowMessage/CaseActivity.php
new file mode 100644 (file)
index 0000000..59380ba
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * When an activity is created in a case, the "case_activity" email is sent.
+ * Generally, the email is sent to the assignee, although (depending on
+ * the configuration/add-ons) additional copies may be sent.
+ *
+ * @see CRM_Case_BAO_Case::sendActivityCopy
+ */
+class CRM_Case_WorkflowMessage_CaseActivity extends Civi\WorkflowMessage\GenericWorkflowMessage {
+
+  const GROUP = 'msg_tpl_workflow_case';
+  const WORKFLOW = 'case_activity';
+
+  /**
+   * The recipient.
+   *
+   * Example: ['contact_id' => 123, 'display_name' => 'Bob Roberts', role => 'FIXME']
+   *
+   * @var array|null
+   * @scope tokenContext, tplParams
+   * @required
+   */
+  public $contact;
+
+  /**
+   * @var int
+   * @scope tplParams as client_id
+   * @required
+   */
+  public $clientId;
+
+  /**
+   * @var string
+   * @scope tplParams
+   * @required
+   */
+  public $activitySubject;
+
+  /**
+   * @var string
+   * @scope tplParams
+   * @required
+   */
+  public $activityTypeName;
+
+  /**
+   * Unique ID for this activity. Unique and difficult to guess.
+   *
+   * @var string
+   * @scope tplParams
+   * @required
+   */
+  public $idHash;
+
+  /**
+   * @var bool
+   * @scope tplParams
+   * @required
+   */
+  public $isCaseActivity;
+
+  /**
+   * @var string
+   * @scope tplParams
+   */
+  public $editActURL;
+
+  /**
+   * @var string
+   * @scope tplParams
+   */
+  public $viewActURL;
+
+  /**
+   * @var string
+   * @scope tplParams
+   */
+  public $manageCaseURL;
+
+  /**
+   * List of conventional activity fields.
+   *
+   * Example: [['label' => ..., 'category' => ..., 'type' => ..., 'value' => ...]]
+   *
+   * @var array
+   * @scope tplParams as activity.fields
+   * @required
+   */
+  public $activityFields;
+
+  /**
+   * List of custom activity fields, grouped by CustomGroup.
+   *
+   * Example: ['My Custom Stuff' => [['label' => ..., 'category' => ..., 'type' => ..., 'value' => ...]]]
+   *
+   * @var array
+   * @scope tplParams as activity.customGroups
+   */
+  public $activityCustomGroups = [];
+
+}
diff --git a/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php
new file mode 100644 (file)
index 0000000..47c9900
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+return [
+  'title' => ts('Case Activity (Adhoc-style example)'),
+  'tags' => [],
+  'data' => function (\Civi\WorkflowMessage\Examples $examples) {
+    $contact = $examples->extend('generic.alex.data.modelProps.contact', [
+      'role' => 'myrole',
+    ]);
+    return [
+      'tokenContext' => [
+        'contact' => $contact,
+      ],
+      'tplParams' => [
+        'contact' => $contact,
+        'isCaseActivity' => 1,
+        'client_id' => 101,
+        'activityTypeName' => 'Follow up',
+        'activitySubject' => 'Test 123',
+        'idHash' => 'abcdefg',
+        'activity' => [
+          'fields' => [
+            [
+              'label' => 'Case ID',
+              'type' => 'String',
+              'value' => '1234',
+            ],
+          ],
+        ],
+      ],
+    ];
+  },
+];
diff --git a/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php
new file mode 100644 (file)
index 0000000..65f413e
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+return [
+  'title' => ts('Case Activity (Class-style example)'),
+  'tags' => ['phpunit', 'preview'],
+  'data' => function (\Civi\WorkflowMessage\Examples $examples) {
+    return $examples->extend('generic.alex.data', [
+      'modelProps' => [
+        'contact' => [
+          'role' => 'myrole',
+        ],
+        'isCaseActivity' => 1,
+        'clientId' => 101,
+        'activityTypeName' => 'Follow up',
+        'activityFields' => [
+          [
+            'label' => 'Case ID',
+            'type' => 'String',
+            'value' => '1234',
+          ],
+        ],
+        'activitySubject' => 'Test 123',
+        'activityCustomGroups' => [],
+        'idHash' => 'abcdefg',
+      ],
+    ]);
+  },
+  'asserts' => [
+    'default' => [
+      ['for' => 'subject', 'regex' => '/\[case #abcdefg\] Test 123/'],
+      ['for' => 'text', 'regex' => '/Your Case Role\(s\) : myrole/'],
+    ],
+  ],
+];
diff --git a/Civi/Test/WorkflowMessageTestTrait.php b/Civi/Test/WorkflowMessageTestTrait.php
new file mode 100644 (file)
index 0000000..395721b
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Test;
+
+use Civi\WorkflowMessage\WorkflowMessage;
+
+trait WorkflowMessageTestTrait {
+
+  abstract public function getWorkflowClass(): string;
+
+  public function getWorkflowName(): string {
+    $class = $this->getWorkflowClass();
+    return $class::WORKFLOW;
+  }
+
+  /**
+   * @return \Civi\Api4\Generic\AbstractGetAction
+   * @throws \API_Exception
+   */
+  protected function findExamples(): \Civi\Api4\Generic\AbstractGetAction {
+    return \Civi\Api4\WorkflowMessageExample::get(0)
+      ->setSelect(['name', 'title', 'workflow', 'tags', 'data', 'asserts'])
+      ->addWhere('workflow', '=', $this->getWorkflowName())
+      ->addWhere('tags', 'CONTAINS', 'phpunit');
+  }
+
+  /**
+   * @param array $exampleProps
+   * @param string $exampleName
+   * @throws \Civi\WorkflowMessage\Exception\WorkflowMessageException
+   */
+  protected function assertConstructorEquivalence(array $exampleProps, $exampleName = ''): void {
+    $class = $this->getWorkflowClass();
+    $instances = [];
+    $instances["factory_$exampleName"] = WorkflowMessage::create($this->getWorkflowName(), $exampleProps);
+    $instances["class_$exampleName"] = new $class($exampleProps);
+
+    /** @var \Civi\WorkflowMessage\WorkflowMessageInterface $refInstance */
+    /** @var \Civi\WorkflowMessage\WorkflowMessageInterface $cmpInstance */
+
+    $refName = $refInstance = NULL;
+    $comparisons = 0;
+    foreach ($instances as $cmpName => $cmpInstance) {
+      if ($refName === NULL) {
+        $refName = $cmpName;
+        $refInstance = $cmpInstance;
+        continue;
+      }
+
+      $this->assertSameWorkflowMessage($refInstance, $cmpInstance, "Compare $refName vs $cmpName: ");
+      $comparisons++;
+    }
+    $this->assertEquals(1, $comparisons);
+  }
+
+  /**
+   * @param \Civi\WorkflowMessage\WorkflowMessageInterface $refInstance
+   * @param \Civi\WorkflowMessage\WorkflowMessageInterface $cmpInstance
+   * @param string|null $prefix
+   */
+  protected function assertSameWorkflowMessage(\Civi\WorkflowMessage\WorkflowMessageInterface $refInstance, \Civi\WorkflowMessage\WorkflowMessageInterface $cmpInstance, ?string $prefix = NULL): void {
+    if ($prefix === NULL) {
+      $prefix = sprintf('[%s] ', $this->getWorkflowName());
+    }
+    $this->assertEquals($refInstance->export('tplParams'), $cmpInstance->export('tplParams'), "{$prefix}Should have same export(tplParams)");
+    $this->assertEquals($refInstance->export('tokenContext'), $cmpInstance->export('tokenContext'), "{$prefix}should have same export(tokenContext)");
+    $this->assertEquals($refInstance->export('envelope'), $cmpInstance->export('envelope'), "{$prefix}Should have same export(envelope)");
+    $refExportAll = WorkflowMessage::exportAll($refInstance);
+    $cmpExportAll = WorkflowMessage::exportAll($cmpInstance);
+    $this->assertEquals($refExportAll, $cmpExportAll, "{$prefix}Should have same exportAll()");
+  }
+
+}
index 417450ef93b3f7f19bf3e84f3c52c05ff4ef0b83..8e9abec172873d41d85d7024e896550f2d6ccbaa 100644 (file)
@@ -303,7 +303,7 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
     $e->string = \CRM_Utils_Token::replaceDomainTokens($e->string, $domain, $isHtml, $e->message['tokens'], $useSmarty);
 
     if (!empty($e->context['contact'])) {
-      \CRM_Utils_Token::replaceGreetingTokens($e->string, $e->context['contact'], $e->context['contact']['contact_id'], NULL, $useSmarty);
+      \CRM_Utils_Token::replaceGreetingTokens($e->string, $e->context['contact'], $e->context['contact']['contact_id'] ?? $e->context['contactId'], NULL, $useSmarty);
     }
 
     if ($useSmarty) {
diff --git a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php
new file mode 100644 (file)
index 0000000..5a21f5b
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Class CRM_Case_WorkflowMessage_CaseActivityTest
+ */
+class CRM_Case_WorkflowMessage_CaseActivityTest extends CiviUnitTestCase {
+  use \Civi\Test\WorkflowMessageTestTrait;
+
+  public function getWorkflowClass(): string {
+    return CRM_Case_WorkflowMessage_CaseActivity::class;
+  }
+
+  public function testAdhocClassEquiv() {
+    $examples = \Civi\Api4\WorkflowMessageExample::get(0)
+      ->setSelect(['name', 'data'])
+      ->addWhere('name', 'IN', ['case_activity.adhoc_1', 'case_activity.class_1'])
+      ->execute()
+      ->indexBy('name')
+      ->column('data');
+    $byAdhoc = Civi\WorkflowMessage\WorkflowMessage::create('case_activity', $examples['case_activity.adhoc_1']);
+    $byClass = new CRM_Case_WorkflowMessage_CaseActivity($examples['case_activity.class_1']);
+    $this->assertSameWorkflowMessage($byClass, $byAdhoc, 'Compare byClass and byAdhoc: ');
+  }
+
+  /**
+   * Ensure that various methods of constructing a WorkflowMessage all produce similar results.
+   *
+   * To see this, we take all the example data and use it with diff constructors.
+   */
+  public function testConstructorEquivalence() {
+    $examples = $this->findExamples()->execute()->indexBy('name')->column('data');
+    $this->assertTrue(count($examples) >= 1, 'Must have at least one example data-set');
+    foreach ($examples as $example) {
+      $this->assertConstructorEquivalence($example);
+    }
+  }
+
+  /**
+   * Basic canary test fetching a specific example.
+   *
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  public function testExampleGet() {
+    $file = \Civi::paths()->getPath('[civicrm.root]/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php');
+    $workflow = 'case_activity';
+    $name = 'case_activity.class_1';
+
+    $this->assertTrue(file_exists($file), "Expect find canary file ($file)");
+
+    $get = \Civi\Api4\WorkflowMessageExample::get()
+      ->addWhere('name', '=', $name)
+      ->execute()
+      ->single();
+    $this->assertEquals($workflow, $get['workflow']);
+    $this->assertTrue(!isset($get['data']));
+    $this->assertTrue(!isset($get['asserts']));
+
+    $get = \Civi\Api4\WorkflowMessageExample::get()
+      ->addWhere('name', '=', $name)
+      ->addSelect('workflow', 'data')
+      ->execute()
+      ->single();
+    $this->assertEquals($workflow, $get['workflow']);
+    $this->assertEquals(100, $get['data']['modelProps']['contact']['contact_id']);
+    $this->assertEquals('myrole', $get['data']['modelProps']['contact']['role']);
+  }
+
+}
index b288a7ca98eca0c92e8068b2573a0ef3528a439e..f3ae760889407b85b868b9adb9d4ff696d21c23c 100644 (file)
@@ -195,38 +195,37 @@ class CRM_Core_BAO_MessageTemplateTest extends CiviUnitTestCase {
     $client_id = $this->individualCreate();
     $contact_id = $this->individualCreate();
 
-    $tplParams = [
-      'isCaseActivity' => 1,
-      'client_id' => $client_id,
-      // activityTypeName means label here not name, but it's ok because label is desired here (dev/core#1116-ok-label)
-      'activityTypeName' => 'Follow up',
-      'activity' => [
-        'fields' => [
+    $msg = \Civi\WorkflowMessage\WorkflowMessage::create('case_activity', [
+      'modelProps' => [
+        'contactId' => $contact_id,
+        'contact' => ['role' => 'Sand grain counter'],
+        'isCaseActivity' => 1,
+        'clientId' => $client_id,
+        // activityTypeName means label here not name, but it's ok because label is desired here (dev/core#1116-ok-label)
+        'activityTypeName' => 'Follow up',
+        'activityFields' => [
           [
             'label' => 'Case ID',
             'type' => 'String',
             'value' => '1234',
           ],
         ],
+        'activitySubject' => 'Test 123',
+        'idHash' => substr(sha1(CIVICRM_SITE_KEY . '1234'), 0, 7),
       ],
-      'activitySubject' => 'Test 123',
-      'idHash' => substr(sha1(CIVICRM_SITE_KEY . '1234'), 0, 7),
-      'contact' => ['role' => 'Sand grain counter'],
-    ];
+    ]);
 
-    [, $subject, $message] = CRM_Core_BAO_MessageTemplate::sendTemplate(
-      [
-        'valueName' => 'case_activity',
-        'contactId' => $contact_id,
-        'tplParams' => $tplParams,
-        'from' => 'admin@example.com',
-        'toName' => 'Demo',
-        'toEmail' => 'admin@example.com',
-        'attachments' => NULL,
-      ]
-    );
+    $this->assertEquals([], \Civi\Test\Invasive::get([$msg, '_extras']));
+
+    [, $subject, $message] = $msg->sendTemplate([
+      'valueName' => 'case_activity',
+      'from' => 'admin@example.com',
+      'toName' => 'Demo',
+      'toEmail' => 'admin@example.com',
+      'attachments' => NULL,
+    ]);
 
-    $this->assertEquals('[case #' . $tplParams['idHash'] . '] Test 123', $subject);
+    $this->assertEquals('[case #' . $msg->getIdHash() . '] Test 123', $subject);
     $this->assertStringContainsString('Your Case Role(s) : Sand grain counter', $message);
     $this->assertStringContainsString('Case ID : 1234', $message);
   }