Update test for runner
[civicrm-core.git] / tests / phpunit / CRM / Mailing / MailingSystemTest.php
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
12 /**
13 * Test that content produced by CiviMail looks the way it's expected.
14 *
15 * @package CiviCRM_APIv3
16 * @subpackage API_Job
17 *
18 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
20 *
21 */
22
23 /**
24 * Class CRM_Mailing_MailingSystemTest.
25 *
26 * This class tests the deprecated code that we are moving
27 * away from supporting.
28 *
29 * MailingSystemTest checks that overall composition and delivery of
30 * CiviMail blasts works. It extends CRM_Mailing_BaseMailingSystemTest
31 * which provides the general test scenarios -- but this variation
32 * checks that certain internal events/hooks fire.
33 *
34 * MailingSystemTest is the counterpart to FlexMailerSystemTest.
35 *
36 * @group headless
37 * @group civimail
38 * @see \Civi\FlexMailer\FlexMailerSystemTest
39 */
40 class CRM_Mailing_MailingSystemTest extends CRM_Mailing_BaseMailingSystemTest {
41
42 private $counts;
43
44 /**
45 * Set up the deprecated bao support.
46 */
47 public function setUp(): void {
48 parent::setUp();
49 // If we happen to execute with flexmailer active, use BAO mode.
50 // There is a parallel FlexMailerSystemTest which runs in flexmailer mode.
51 Civi::settings()->add(['flexmailer_traditional' => 'bao']);
52
53 $hooks = \CRM_Utils_Hook::singleton();
54 $hooks->setHook('civicrm_alterMailParams',
55 [$this, 'hook_alterMailParams']);
56 error_reporting(E_ALL && !E_USER_DEPRECATED);
57 }
58
59 /**
60 * @see CRM_Utils_Hook::alterMailParams
61 */
62 public function hook_alterMailParams(&$params, $context = NULL): void {
63 $this->counts['hook_alterMailParams'] = 1;
64 $this->assertEquals('civimail', $context);
65 }
66
67 /**
68 * Post test cleanup.
69 */
70 public function tearDown(): void {
71 global $dbLocale;
72 if ($dbLocale) {
73 CRM_Core_I18n_Schema::makeSinglelingual('en_US');
74 }
75 parent::tearDown();
76 $this->assertNotEmpty($this->counts['hook_alterMailParams']);
77 }
78
79 /**
80 * Test legacy mailer preview functionality.
81 */
82 public function testMailerPreviewExtraScheme(): void {
83 $contactID = $this->individualCreate();
84 $displayName = $this->callAPISuccess('contact', 'get', ['id' => $contactID]);
85 $displayName = $displayName['values'][$contactID]['display_name'];
86 $this->assertNotEmpty($displayName);
87
88 $params = $this->_params;
89 $params['body_html'] = '<a href="http://{action.forward}">Forward this email written in ckeditor</a>';
90 $params['api.Mailing.preview'] = [
91 'id' => '$value.id',
92 'contact_id' => $contactID,
93 ];
94 $params['options']['force_rollback'] = 1;
95
96 $result = $this->callAPISuccess('mailing', 'create', $params);
97 $previewResult = $result['values'][$result['id']]['api.Mailing.preview'];
98 $this->assertRegexp('!>Forward this email written in ckeditor</a>!', $previewResult['values']['body_html']);
99 $this->assertRegexp('!<a href="([^"]+)civicrm/mailing/forward&amp;amp;reset=1&amp;jid=&amp;qid=&amp;h=\w*">!', $previewResult['values']['body_html']);
100 $this->assertStringNotContainsString("http://http://", $previewResult['values']['body_html']);
101 }
102
103 // ---- Boilerplate ----
104
105 // The remainder of this class contains dummy stubs which make it easier to
106 // work with the tests in an IDE.
107
108 /**
109 * Generate a fully-formatted mailing (with body_html content).
110 *
111 * @dataProvider urlTrackingExamples
112 */
113 public function testUrlTracking(
114 $inputHtml,
115 $htmlUrlRegex,
116 $textUrlRegex,
117 $params
118 ) {
119 parent::testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params);
120 }
121
122 public function testBasicHeaders() {
123 parent::testBasicHeaders();
124 }
125
126 public function testText() {
127 parent::testText();
128 }
129
130 public function testHtmlWithOpenTracking() {
131 parent::testHtmlWithOpenTracking();
132 }
133
134 public function testHtmlWithOpenAndUrlTracking() {
135 parent::testHtmlWithOpenAndUrlTracking();
136 }
137
138 /**
139 * Test to check Activity being created on mailing Job.
140 *
141 */
142 public function testMailingActivityCreate() {
143 $subject = uniqid('testMailingActivityCreate');
144 $this->runMailingSuccess([
145 'subject' => $subject,
146 'body_html' => 'Test Mailing Activity Create',
147 'scheduled_id' => $this->individualCreate(),
148 ]);
149
150 $this->callAPISuccessGetCount('activity', [
151 'activity_type_id' => 'Bulk Email',
152 'status_id' => 'Completed',
153 'subject' => $subject,
154 ], 1);
155 }
156
157 /**
158 * Data provider for testGitLabIssue1108
159 *
160 * First we run it without multiLingual mode, then with.
161 *
162 * This is because we test table names, which may have been translated in a
163 * multiLingual context.
164 *
165 */
166 public function multiLingual() {
167 return [[0], [1]];
168 }
169
170 /**
171 * - unsubscribe used dodgy SQL that only checked half of the polymorphic
172 * relationship in mailing_group, meaning it could match 'mailing 123'
173 * against _group_ 123.
174 *
175 * - also, an INNER JOIN on the group table hid the mailing-based
176 * mailing_group records.
177 *
178 * - in turn this inner join meant the query returned nothing, which then
179 * caused the code that is supposed to find the contact within those groups
180 * to basically find all the groups that the contact in or were smart groups.
181 *
182 * - in certain situations (which I have not been able to replicate in this
183 * test) it caused the unsubscribe to fail to find *any* groups to unsubscribe
184 * people from, thereby breaking the unsubscribe.
185 *
186 * @dataProvider multiLingual
187 *
188 */
189 public function testGitLabIssue1108($isMultiLingual) {
190
191 // We need to make sure the mailing IDs are higher than the groupIDs.
192 // We do this by adding mailings until the mailing.id value is at least 10
193 // higher than the highest group.id
194 // Note that creating a row in a transaction then rolling back the
195 // transaction still increments the AUTO_INCREMENT counter for the table.
196 // (If this behaviour ever changes we throw an exception.)
197 if ($isMultiLingual) {
198 $this->enableMultilingual();
199 CRM_Core_I18n_Schema::addLocale('fr_FR', 'en_US');
200 }
201 $max_group_id = CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM civicrm_group");
202 $max_mailing_id = 0;
203 while ($max_mailing_id < $max_group_id + 10) {
204 CRM_Core_Transaction::create()->run(function($tx) use (&$max_mailing_id) {
205 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_mailing (name) VALUES ('dummy');");
206 $_ = (int) CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM civicrm_mailing");
207 if ($_ === $max_mailing_id) {
208 throw new RuntimeException("Expected that creating a new row would increment ID, but it did not. This could be a change in MySQL's implementation of rollback");
209 }
210 $max_mailing_id = $_;
211 $tx->rollback();
212 });
213 }
214
215 // Because our parent class marks the _groupID as private, we can't use that :-(
216 $group_1 = $this->groupCreate([
217 'name' => 'Test Group 1108.1',
218 'title' => 'Test Group 1108.1',
219 ]);
220 $this->createContactsInGroup(2, $group_1);
221
222 // Also _mut is private to the parent, so we have to make our own:
223 $mut = new CiviMailUtils($this, TRUE);
224
225 // Create initial mailing to the group.
226 $mailingParams = [
227 'name' => 'Issue 1108: mailing 1',
228 'subject' => 'Issue 1108: mailing 1',
229 'created_id' => 1,
230 'groups' => ['include' => [$group_1]],
231 'scheduled_date' => 'now',
232 'body_text' => 'Please just {action.unsubscribe}',
233 ];
234
235 // The following code is exactly the same as runMailingSuccess() except that we store the ID of the mailing.
236 $mailing_1 = $this->callAPISuccess('mailing', 'create', $mailingParams);
237 $mut->assertRecipients(array());
238 $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
239
240 $allMessages = $mut->getAllMessages('ezc');
241 // There are exactly two contacts produced by setUp().
242 $this->assertEquals(2, count($allMessages));
243
244 // We need a new group
245 $group_2 = $this->groupCreate([
246 'name' => 'Test Group 1108.2',
247 'title' => 'Test Group 1108.2',
248 ]);
249
250 // Now create the 2nd mailing to the recipients of the first,
251 // excluding our new albeit empty group.
252 $mailingParams = [
253 'name' => 'Issue 1108: mailing 2',
254 'subject' => 'Issue 1108: mailing 2',
255 'created_id' => 1,
256 'mailings' => ['include' => [$mailing_1['id']]],
257 'groups' => ['exclude' => [$group_2]],
258 'scheduled_date' => 'now',
259 'body_text' => 'Please just {action.unsubscribeUrl}',
260 ];
261 $this->callAPISuccess('mailing', 'create', $mailingParams);
262 $_ = $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
263
264 $allMessages = $mut->getAllMessages('ezc');
265 // We should have 2+2 messages sent by the mail system now.
266 $this->assertEquals(4, count($allMessages));
267
268 // So far so good.
269 // Now extract the unsubscribe details.
270 $message = end($allMessages);
271 $this->assertTrue($message->body instanceof ezcMailText);
272 $this->assertEquals('plain', $message->body->subType);
273 $this->assertEquals(1, preg_match(
274 '@mailing/unsubscribe.*jid=(\d+)&qid=(\d+)&h=([0-9a-z]+)@',
275 $message->body->text,
276 $matches
277 ));
278
279 // Create a group that has nothing to do with this mailing.
280 $group_3 = $this->groupCreate([
281 'name' => 'Test Group 1108.3',
282 'title' => 'Test Group 1108.3',
283 ]);
284 // Add contacts from group 1 to group 3.
285 $gcQuery = new CRM_Contact_BAO_GroupContact();
286 $gcQuery->group_id = $group_1;
287 $gcQuery->status = 'Added';
288 $gcQuery->find();
289 while ($gcQuery->fetch()) {
290 $this->callAPISuccess('group_contact', 'create',
291 ['group_id' => $group_3, 'contact_id' => $gcQuery->contact_id, 'status' => 'Added']);
292 }
293
294 // Part of the issue is caused by the fact that (at time of writing) the
295 // SQL joined the mailing_group table on just the entity_id, assuming it to
296 // be a group, but actually it could be a mailing.
297 // The difficulty in testing this is that because all our IDs are very low
298 // and contiguous the SQL looking for a match for 'mailing 1' does match a
299 // group ID of '1', which is created in this class's parent's setUp().
300 // Strictly speaking we don't know that it has ID 1, but as we can't access _groupID
301 // we'll have to assume that.
302 //
303 // So by deleting that group the SQL then matches nothing which is what we
304 // need for this case.
305 $_ = new CRM_Contact_BAO_Group();
306 $_->id = 1;
307 $_->delete();
308
309 $hooks = \CRM_Utils_Hook::singleton();
310 $found = [];
311 $hooks->setHook('civicrm_unsubscribeGroups',
312 function ($op, $mailingId, $contactId, &$groups, &$baseGroups) use (&$found) {
313 $found['groups'] = $groups;
314 $found['baseGroups'] = $baseGroups;
315 });
316
317 // Now test unsubscribe groups.
318 $groups = CRM_Mailing_Event_BAO_Unsubscribe::unsub_from_mailing(
319 $matches[1],
320 $matches[2],
321 $matches[3],
322 TRUE
323 );
324
325 // We expect that our group_1 was found.
326 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
327
328 // We *should* get an array with just our $group_1 since this is the only group
329 // that we have included.
330 // $group_2 was only used to exclude people.
331 // $group_3 has nothing to do with this mailing and should not be there.
332 $this->assertNotEmpty($groups, "We should have received an array.");
333 $this->assertEquals([$group_1], array_keys($groups),
334 "We should have received an array with our group 1 in it.");
335
336 if ($isMultiLingual) {
337 global $dbLocale;
338 $dbLocale = '_fr_FR';
339 // Now test unsubscribe groups.
340 $groups = CRM_Mailing_Event_BAO_Unsubscribe::unsub_from_mailing(
341 $matches[1],
342 $matches[2],
343 $matches[3],
344 TRUE
345 );
346
347 // We expect that our group_1 was found.
348 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
349
350 // We *should* get an array with just our $group_1 since this is the only group
351 // that we have included.
352 // $group_2 was only used to exclude people.
353 // $group_3 has nothing to do with this mailing and should not be there.
354 $this->assertNotEmpty($groups, "We should have received an array.");
355 $this->assertEquals([$group_1], array_keys($groups),
356 "We should have received an array with our group 1 in it.");
357 global $dbLocale;
358 $dbLocale = '_en_US';
359 }
360 }
361
362 }