2 namespace Civi\ActionSchedule
;
5 * The AbstractMappingTest is a base class which can help define new
6 * tests for scheduled-reminders.
8 * Generally, the problem of testing scheduled-reminders is one of permutations --
9 * there are many different types of records, fields on the records, and scheduling options.
10 * To test these, we setup a schedule of cron-runs (eg Jan 20 to Mar 1) and create some example
13 * To setup the examples, we need to string together several helper functions, like:
15 * - startOnTime(), startWeekBefore(), or startWeekAfter()
16 * - repeatTwoWeeksAfter()
17 * - limitToRecipientAlice(), limitToRecipientBob(), alsoRecipientBob()
18 * - addAliceDues(), addBobDonation()
19 * - addAliceMeeting(), addBobPhoneCall()
21 * (Some of these helpers are defined in AbstractMappingTest. Some are defined in subclasses.)
23 * Concrete subclasses should implement a few elements:
25 * - Optionally, modify $cronSchedule to specify when the cron jobs run.
26 * (By default, it specifies daily from 20-Jan-15 to 1-Mar-15.)
27 * - Implement at least one setup-helper which creates example records.
28 * The example records should use the specified date (`$this->targetDate`)
29 * and should relate to `$this->contact['alice']` (or 'bob 'or 'carol').
30 * - Implement at least one schedule-helper which configures `$this->schedule`
31 * to use the preferred action mapping. It may define various
32 * filters, such as value-filters, status-filters, or recipient-filters.
33 * - Implement `createTestCases()` which defines various
34 * permutations of tests to run. Each test provides a list of emails
35 * which should be fired (datetime/recipient/subject).
38 * @see CRM_Contribute_ActionMapping_ByTypeTest
39 * @see CRM_Activity_ActionMappingTest
41 abstract class AbstractMappingTest
extends \CiviUnitTestCase
{
44 * @var \CRM_Core_DAO_ActionSchedule
49 * The date which should be stored on the matching record in the DB.
56 * Example contact records.
63 * The schedule for invoking cron.
68 * - interval: int, seconds
73 * When comparing timestamps, treat them as the same if they
74 * occur within a certain distance of each other.
78 public $dateTolerance = 120;
86 * Generate a list of test cases, where each is a distinct combination of
87 * data, schedule-rules, and schedule results.
90 * - targetDate: string; eg "2015-02-01 00:00:01"
91 * - setupFuncs: string, space-separated list of setup functions
92 * - messages: array; each item is a message that's expected to be sent
93 * each message may include keys:
94 * - time: approximate time (give or take a few seconds)
98 abstract public function createTestCases();
100 // ---------------------------------------- Setup Helpers ----------------------------------------
103 * Send first message on the designated date.
105 public function startOnTime() {
106 $this->schedule
->start_action_condition
= 'before';
107 $this->schedule
->start_action_offset
= '0';
108 $this->schedule
->start_action_unit
= 'day';
112 * Send first message one week before designated date.
114 public function startWeekBefore() {
115 $this->schedule
->start_action_condition
= 'before';
116 $this->schedule
->start_action_offset
= '7';
117 $this->schedule
->start_action_unit
= 'day';
121 * Send first message one week after designated date.
123 public function startWeekAfter() {
124 $this->schedule
->start_action_condition
= 'after';
125 $this->schedule
->start_action_offset
= '7';
126 $this->schedule
->start_action_unit
= 'day';
130 * Send repeated messages until two weeks after designated date.
132 public function repeatTwoWeeksAfter() {
133 $this->schedule
->is_repeat
= 1;
134 $this->schedule
->repetition_frequency_interval
= '7';
135 $this->schedule
->repetition_frequency_unit
= 'day';
137 $this->schedule
->end_action
= 'after';
138 $this->schedule
->end_date
= $this->schedule
->start_action_date
;
139 $this->schedule
->end_frequency_interval
= '14';
140 $this->schedule
->end_frequency_unit
= 'day';
144 * Compose a "Hello" email which includes the recipient's first name.
146 public function useHelloFirstName() {
147 $this->schedule
->subject
= 'Hello, {contact.first_name}. (via subject)';
148 $this->schedule
->body_html
= '<p>Hello, {contact.first_name}. (via body_html)</p>';
149 $this->schedule
->body_text
= 'Hello, {contact.first_name}. (via body_text)';
153 * Limit possible recipients to Alice.
155 public function limitToRecipientAlice() {
156 $this->schedule
->limit_to
= 1;
157 $this->schedule
->recipient
= NULL;
158 $this->schedule
->recipient_listing
= NULL;
159 $this->schedule
->recipient_manual
= $this->contacts
['alice']['id'];
163 * Limit possible recipients to Bob.
165 public function limitToRecipientBob() {
166 $this->schedule
->limit_to
= 1;
167 $this->schedule
->recipient
= NULL;
168 $this->schedule
->recipient_listing
= NULL;
169 $this->schedule
->recipient_manual
= $this->contacts
['bob']['id'];
173 * Also include recipient Bob.
175 public function alsoRecipientBob() {
176 $this->schedule
->limit_to
= 0;
177 $this->schedule
->recipient
= NULL;
178 $this->schedule
->recipient_listing
= NULL;
179 $this->schedule
->recipient_manual
= $this->contacts
['bob']['id'];
182 // ---------------------------------------- Core test definitions ----------------------------------------
185 * Setup an empty schedule and some contacts.
187 protected function setUp() {
189 $this->useTransaction();
191 $this->mut
= new \
CiviMailUtils($this, TRUE);
193 $this->cronSchedule
= [
194 'start' => '2015-01-20 00:00:00',
195 'end' => '2015-03-01 00:00:00',
197 'interval' => 24 * 60 * 60,
200 $this->schedule
= new \
CRM_Core_DAO_ActionSchedule();
201 $this->schedule
->title
= $this->getName(TRUE);
202 $this->schedule
->name
= \CRM_Utils_String
::munge($this->schedule
->title
);
203 $this->schedule
->is_active
= 1;
204 $this->schedule
->group_id
= NULL;
205 $this->schedule
->recipient
= NULL;
206 $this->schedule
->recipient_listing
= NULL;
207 $this->schedule
->recipient_manual
= NULL;
208 $this->schedule
->absolute_date
= NULL;
209 $this->schedule
->msg_template_id
= NULL;
210 $this->schedule
->record_activity
= NULL;
212 $this->contacts
['alice'] = $this->callAPISuccess('Contact', 'create', [
213 'contact_type' => 'Individual',
214 'first_name' => 'Alice',
215 'last_name' => 'Exemplar',
216 'email' => 'alice@example.org',
218 $this->contacts
['bob'] = $this->callAPISuccess('Contact', 'create', [
219 'contact_type' => 'Individual',
220 'first_name' => 'Bob',
221 'last_name' => 'Exemplar',
222 'email' => 'bob@example.org',
224 $this->contacts
['carol'] = $this->callAPISuccess('Contact', 'create', [
225 'contact_type' => 'Individual',
226 'first_name' => 'Carol',
227 'last_name' => 'Exemplar',
228 'email' => 'carol@example.org',
230 $this->contacts
['dave'] = $this->callAPISuccess('Contact', 'create', [
231 'contact_type' => 'Individual',
232 'first_name' => 'Dave',
233 'last_name' => 'Exemplar',
234 'email' => 'dave@example.org',
237 $this->contacts
['edith'] = $this->callAPISuccess('Contact', 'create', [
238 'contact_type' => 'Individual',
239 'first_name' => 'Edith',
240 'last_name' => 'Exemplar',
241 'email' => 'edith@example.org',
244 $this->contacts
['francis'] = $this->callAPISuccess('Contact', 'create', [
245 'contact_type' => 'Individual',
246 'first_name' => 'Francis',
247 'last_name' => 'Exemplar',
248 'api.Email.create' => [
249 'email' => 'frances@example.org',
256 * Execute the default schedule, without any special recipient selections.
258 * @dataProvider createTestCases
260 * @param string $targetDate
261 * @param string $setupFuncs
262 * @param array $expectMessages
266 public function testDefault($targetDate, $setupFuncs, $expectMessages) {
267 $this->targetDate
= $targetDate;
269 foreach (explode(' ', $setupFuncs) as $setupFunc) {
270 $this->{$setupFunc}();
272 $this->schedule
->save();
274 $actualMessages = [];
275 foreach ($this->cronTimes() as $time) {
276 \CRM_Utils_Time
::setTime($time);
277 $this->callAPISuccess('job', 'send_reminder', []);
278 foreach ($this->mut
->getAllMessages('ezc') as $message) {
279 /** @var \ezcMail $message */
282 'to' => \CRM_Utils_Array
::collect('email', $message->to
),
283 'subject' => $message->subject
,
285 sort($simpleMessage['to']);
286 $actualMessages[] = $simpleMessage;
287 $this->mut
->clearMessages();
291 $errorText = "Incorrect messages: " . print_r([
292 'actualMessages' => $actualMessages,
293 'expectMessages' => $expectMessages,
295 $this->assertEquals(count($expectMessages), count($actualMessages), $errorText);
296 usort($expectMessages, [__CLASS__
, 'compareSimpleMsgs']);
297 usort($actualMessages, [__CLASS__
, 'compareSimpleMsgs']);
298 foreach ($expectMessages as $offset => $expectMessage) {
299 $actualMessage = $actualMessages[$offset];
300 $this->assertApproxEquals(strtotime($expectMessage['time']), strtotime($actualMessage['time']), $this->dateTolerance
, $errorText);
301 if (isset($expectMessage['to'])) {
302 sort($expectMessage['to']);
303 $this->assertEquals($expectMessage['to'], $actualMessage['to'], $errorText);
305 if (isset($expectMessage['subject'])) {
306 $this->assertRegExp($expectMessage['subject'], $actualMessage['subject'], $errorText);
311 protected function cronTimes() {
314 $end = strtotime($this->cronSchedule
['end']);
315 for ($time = strtotime($this->cronSchedule
['start']); $time < $end; $time +
= $this->cronSchedule
['interval']) {
316 $times[] = date('Y-m-d H:i:s', $time +
$skew);
322 protected function compareSimpleMsgs($a, $b) {
323 if ($a['time'] != $b['time']) {
324 return ($a['time'] < $b['time']) ?
1 : -1;
326 if ($a['to'] != $b['to']) {
327 return ($a['to'] < $b['to']) ?
1 : -1;
329 if ($a['subject'] != $b['subject']) {
330 return ($a['subject'] < $b['subject']) ?
1 : -1;