dev/core#1378 Scheduled reminders: test to not send if email is on hold
[civicrm-core.git] / tests / phpunit / Civi / ActionSchedule / AbstractMappingTest.php
1 <?php
2 namespace Civi\ActionSchedule;
3
4 /**
5 * The AbstractMappingTest is a base class which can help define new
6 * tests for scheduled-reminders.
7 *
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
11 * records.
12 *
13 * To setup the examples, we need to string together several helper functions, like:
14 *
15 * - startOnTime(), startWeekBefore(), or startWeekAfter()
16 * - repeatTwoWeeksAfter()
17 * - limitToRecipientAlice(), limitToRecipientBob(), alsoRecipientBob()
18 * - addAliceDues(), addBobDonation()
19 * - addAliceMeeting(), addBobPhoneCall()
20 *
21 * (Some of these helpers are defined in AbstractMappingTest. Some are defined in subclasses.)
22 *
23 * Concrete subclasses should implement a few elements:
24 *
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).
36 *
37 * For examples:
38 * @see CRM_Contribute_ActionMapping_ByTypeTest
39 * @see CRM_Activity_ActionMappingTest
40 */
41 abstract class AbstractMappingTest extends \CiviUnitTestCase {
42
43 /**
44 * @var \CRM_Core_DAO_ActionSchedule
45 */
46 public $schedule;
47
48 /**
49 * The date which should be stored on the matching record in the DB.
50 *
51 * @var string
52 */
53 public $targetDate;
54
55 /**
56 * Example contact records.
57 *
58 * @var array
59 */
60 public $contacts;
61
62 /**
63 * The schedule for invoking cron.
64 *
65 * @var array
66 * - start: string
67 * - end: string
68 * - interval: int, seconds
69 */
70 public $cronSchedule;
71
72 /**
73 * When comparing timestamps, treat them as the same if they
74 * occur within a certain distance of each other.
75 *
76 * @var int seconds
77 */
78 public $dateTolerance = 120;
79
80 /**
81 * @var \CiviMailUtils
82 */
83 public $mut;
84
85 /**
86 * Generate a list of test cases, where each is a distinct combination of
87 * data, schedule-rules, and schedule results.
88 *
89 * @return array
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)
95 * - subject: regex
96 * - message: regex
97 */
98 abstract public function createTestCases();
99
100 // ---------------------------------------- Setup Helpers ----------------------------------------
101
102 /**
103 * Send first message on the designated date.
104 */
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';
109 }
110
111 /**
112 * Send first message one week before designated date.
113 */
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';
118 }
119
120 /**
121 * Send first message one week after designated date.
122 */
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';
127 }
128
129 /**
130 * Send repeated messages until two weeks after designated date.
131 */
132 public function repeatTwoWeeksAfter() {
133 $this->schedule->is_repeat = 1;
134 $this->schedule->repetition_frequency_interval = '7';
135 $this->schedule->repetition_frequency_unit = 'day';
136
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';
141 }
142
143 /**
144 * Compose a "Hello" email which includes the recipient's first name.
145 */
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)';
150 }
151
152 /**
153 * Limit possible recipients to Alice.
154 */
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'];
160 }
161
162 /**
163 * Limit possible recipients to Bob.
164 */
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'];
170 }
171
172 /**
173 * Also include recipient Bob.
174 */
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'];
180 }
181
182 // ---------------------------------------- Core test definitions ----------------------------------------
183
184 /**
185 * Setup an empty schedule and some contacts.
186 */
187 protected function setUp() {
188 parent::setUp();
189 $this->useTransaction();
190
191 $this->mut = new \CiviMailUtils($this, TRUE);
192
193 $this->cronSchedule = [
194 'start' => '2015-01-20 00:00:00',
195 'end' => '2015-03-01 00:00:00',
196 // seconds
197 'interval' => 24 * 60 * 60,
198 ];
199
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;
211
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',
217 ]);
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',
223 ]);
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',
229 ]);
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',
235 'do_not_email' => 1,
236 ]);
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',
242 'is_deceased' => 1,
243 ]);
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',
250 'on_hold' => 1,
251 ],
252 ]);
253 }
254
255 /**
256 * Execute the default schedule, without any special recipient selections.
257 *
258 * @dataProvider createTestCases
259 *
260 * @param string $targetDate
261 * @param string $setupFuncs
262 * @param array $expectMessages
263 *
264 * @throws \Exception
265 */
266 public function testDefault($targetDate, $setupFuncs, $expectMessages) {
267 $this->targetDate = $targetDate;
268
269 foreach (explode(' ', $setupFuncs) as $setupFunc) {
270 $this->{$setupFunc}();
271 }
272 $this->schedule->save();
273
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 */
280 $simpleMessage = [
281 'time' => $time,
282 'to' => \CRM_Utils_Array::collect('email', $message->to),
283 'subject' => $message->subject,
284 ];
285 sort($simpleMessage['to']);
286 $actualMessages[] = $simpleMessage;
287 $this->mut->clearMessages();
288 }
289 }
290
291 $errorText = "Incorrect messages: " . print_r([
292 'actualMessages' => $actualMessages,
293 'expectMessages' => $expectMessages,
294 ], 1);
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);
304 }
305 if (isset($expectMessage['subject'])) {
306 $this->assertRegExp($expectMessage['subject'], $actualMessage['subject'], $errorText);
307 }
308 }
309 }
310
311 protected function cronTimes() {
312 $skew = 0;
313 $times = [];
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);
317 //$skew++;
318 }
319 return $times;
320 }
321
322 protected function compareSimpleMsgs($a, $b) {
323 if ($a['time'] != $b['time']) {
324 return ($a['time'] < $b['time']) ? 1 : -1;
325 }
326 if ($a['to'] != $b['to']) {
327 return ($a['to'] < $b['to']) ? 1 : -1;
328 }
329 if ($a['subject'] != $b['subject']) {
330 return ($a['subject'] < $b['subject']) ? 1 : -1;
331 }
332 }
333
334 }