Merge pull request #13078 from agh1/contactdetail-no-or2
[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 * MailingSystemTest checks that overall composition and delivery of
27 * CiviMail blasts works. It extends CRM_Mailing_BaseMailingSystemTest
28 * which provides the general test scenarios -- but this variation
29 * checks that certain internal events/hooks fire.
30 *
31 * MailingSystemTest is the counterpart to FlexMailerSystemTest.
32 *
33 * @group headless
34 * @group civimail
35 * @see \Civi\FlexMailer\FlexMailerSystemTest
36 */
37 class CRM_Mailing_MailingSystemTest extends CRM_Mailing_BaseMailingSystemTest {
38
39 private $counts;
40
41 public function setUp() {
42 parent::setUp();
43 Civi::settings()->add(['experimentalFlexMailerEngine' => FALSE]);
44
45 $hooks = \CRM_Utils_Hook::singleton();
46 $hooks->setHook('civicrm_alterMailParams',
47 [$this, 'hook_alterMailParams']);
48 }
49
50 /**
51 * @see CRM_Utils_Hook::alterMailParams
52 */
53 public function hook_alterMailParams(&$params, $context = NULL) {
54 $this->counts['hook_alterMailParams'] = 1;
55 $this->assertEquals('civimail', $context);
56 }
57
58 public function tearDown() {
59 global $dbLocale;
60 if ($dbLocale) {
61 CRM_Core_I18n_Schema::makeSinglelingual('en_US');
62 }
63 parent::tearDown();
64 $this->assertNotEmpty($this->counts['hook_alterMailParams']);
65 }
66
67 // ---- Boilerplate ----
68
69 // The remainder of this class contains dummy stubs which make it easier to
70 // work with the tests in an IDE.
71
72 /**
73 * Generate a fully-formatted mailing (with body_html content).
74 *
75 * @dataProvider urlTrackingExamples
76 */
77 public function testUrlTracking(
78 $inputHtml,
79 $htmlUrlRegex,
80 $textUrlRegex,
81 $params
82 ) {
83 parent::testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params);
84 }
85
86 public function testBasicHeaders() {
87 parent::testBasicHeaders();
88 }
89
90 public function testText() {
91 parent::testText();
92 }
93
94 public function testHtmlWithOpenTracking() {
95 parent::testHtmlWithOpenTracking();
96 }
97
98 public function testHtmlWithOpenAndUrlTracking() {
99 parent::testHtmlWithOpenAndUrlTracking();
100 }
101
102 /**
103 * Test to check Activity being created on mailing Job.
104 *
105 */
106 public function testMailingActivityCreate() {
107 $subject = uniqid('testMailingActivityCreate');
108 $this->runMailingSuccess([
109 'subject' => $subject,
110 'body_html' => 'Test Mailing Activity Create',
111 'scheduled_id' => $this->individualCreate(),
112 ]);
113
114 $this->callAPISuccessGetCount('activity', [
115 'activity_type_id' => 'Bulk Email',
116 'status_id' => 'Completed',
117 'subject' => $subject,
118 ], 1);
119 }
120
121 /**
122 * Data provider for testGitLabIssue1108
123 *
124 * First we run it without multiLingual mode, then with.
125 *
126 * This is because we test table names, which may have been translated in a
127 * multiLingual context.
128 *
129 */
130 public function multiLingual() {
131 return [[0], [1]];
132 }
133
134 /**
135 * - unsubscribe used dodgy SQL that only checked half of the polymorphic
136 * relationship in mailing_group, meaning it could match 'mailing 123'
137 * against _group_ 123.
138 *
139 * - also, an INNER JOIN on the group table hid the mailing-based
140 * mailing_group records.
141 *
142 * - in turn this inner join meant the query returned nothing, which then
143 * caused the code that is supposed to find the contact within those groups
144 * to basically find all the groups that the contact in or were smart groups.
145 *
146 * - in certain situations (which I have not been able to replicate in this
147 * test) it caused the unsubscribe to fail to find *any* groups to unsubscribe
148 * people from, thereby breaking the unsubscribe.
149 *
150 * @dataProvider multiLingual
151 *
152 */
153 public function testGitLabIssue1108($isMultiLingual) {
154
155 // We need to make sure the mailing IDs are higher than the groupIDs.
156 // We do this by adding mailings until the mailing.id value is at least 10
157 // higher than the highest group.id
158 // Note that creating a row in a transaction then rolling back the
159 // transaction still increments the AUTO_INCREMENT counter for the table.
160 // (If this behaviour ever changes we throw an exception.)
161 if ($isMultiLingual) {
162 $this->enableMultilingual();
163 }
164 $max_group_id = CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM civicrm_group");
165 $max_mailing_id = 0;
166 while ($max_mailing_id < $max_group_id + 10) {
167 CRM_Core_Transaction::create()->run(function($tx) use (&$max_mailing_id) {
168 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_mailing (name) VALUES ('dummy');");
169 $_ = (int) CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM civicrm_mailing");
170 if ($_ === $max_mailing_id) {
171 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");
172 }
173 $max_mailing_id = $_;
174 $tx->rollback();
175 });
176 }
177
178 // Because our parent class marks the _groupID as private, we can't use that :-(
179 $group_1 = $this->groupCreate([
180 'name' => 'Test Group 1108.1',
181 'title' => 'Test Group 1108.1',
182 ]);
183 $this->createContactsInGroup(2, $group_1);
184
185 // Also _mut is private to the parent, so we have to make our own:
186 $mut = new CiviMailUtils($this, TRUE);
187
188 // Create initial mailing to the group.
189 $mailingParams = [
190 'name' => 'Issue 1108: mailing 1',
191 'subject' => 'Issue 1108: mailing 1',
192 'created_id' => 1,
193 'groups' => ['include' => [$group_1]],
194 'scheduled_date' => 'now',
195 'body_text' => 'Please just {action.unsubscribe}',
196 ];
197
198 // The following code is exactly the same as runMailingSuccess() except that we store the ID of the mailing.
199 $mailing_1 = $this->callAPISuccess('mailing', 'create', $mailingParams);
200 $mut->assertRecipients(array());
201 $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
202
203 $allMessages = $mut->getAllMessages('ezc');
204 // There are exactly two contacts produced by setUp().
205 $this->assertEquals(2, count($allMessages));
206
207 // We need a new group
208 $group_2 = $this->groupCreate([
209 'name' => 'Test Group 1108.2',
210 'title' => 'Test Group 1108.2',
211 ]);
212
213 // Now create the 2nd mailing to the recipients of the first,
214 // excluding our new albeit empty group.
215 $mailingParams = [
216 'name' => 'Issue 1108: mailing 2',
217 'subject' => 'Issue 1108: mailing 2',
218 'created_id' => 1,
219 'mailings' => ['include' => [$mailing_1['id']]],
220 'groups' => ['exclude' => [$group_2]],
221 'scheduled_date' => 'now',
222 'body_text' => 'Please just {action.unsubscribeUrl}',
223 ];
224 $this->callAPISuccess('mailing', 'create', $mailingParams);
225 $_ = $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
226
227 $allMessages = $mut->getAllMessages('ezc');
228 // We should have 2+2 messages sent by the mail system now.
229 $this->assertEquals(4, count($allMessages));
230
231 // So far so good.
232 // Now extract the unsubscribe details.
233 $message = end($allMessages);
234 $this->assertTrue($message->body instanceof ezcMailText);
235 $this->assertEquals('plain', $message->body->subType);
236 $this->assertEquals(1, preg_match(
237 '@mailing/unsubscribe.*jid=(\d+)&qid=(\d+)&h=([0-9a-z]+)@',
238 $message->body->text,
239 $matches
240 ));
241
242 // Create a group that has nothing to do with this mailing.
243 $group_3 = $this->groupCreate([
244 'name' => 'Test Group 1108.3',
245 'title' => 'Test Group 1108.3',
246 ]);
247 // Add contacts from group 1 to group 3.
248 $gcQuery = new CRM_Contact_BAO_GroupContact();
249 $gcQuery->group_id = $group_1;
250 $gcQuery->status = 'Added';
251 $gcQuery->find();
252 while ($gcQuery->fetch()) {
253 $this->callAPISuccess('group_contact', 'create',
254 ['group_id' => $group_3, 'contact_id' => $gcQuery->contact_id, 'status' => 'Added']);
255 }
256
257 // Part of the issue is caused by the fact that (at time of writing) the
258 // SQL joined the mailing_group table on just the entity_id, assuming it to
259 // be a group, but actually it could be a mailing.
260 // The difficulty in testing this is that because all our IDs are very low
261 // and contiguous the SQL looking for a match for 'mailing 1' does match a
262 // group ID of '1', which is created in this class's parent's setUp().
263 // Strictly speaking we don't know that it has ID 1, but as we can't access _groupID
264 // we'll have to assume that.
265 //
266 // So by deleting that group the SQL then matches nothing which is what we
267 // need for this case.
268 $_ = new CRM_Contact_BAO_Group();
269 $_->id = 1;
270 $_->delete();
271
272 $hooks = \CRM_Utils_Hook::singleton();
273 $found = [];
274 $hooks->setHook('civicrm_unsubscribeGroups',
275 function ($op, $mailingId, $contactId, &$groups, &$baseGroups) use (&$found) {
276 $found['groups'] = $groups;
277 $found['baseGroups'] = $baseGroups;
278 });
279
280 // Now test unsubscribe groups.
281 $groups = CRM_Mailing_Event_BAO_Unsubscribe::unsub_from_mailing(
282 $matches[1],
283 $matches[2],
284 $matches[3],
285 TRUE
286 );
287
288 // We expect that our group_1 was found.
289 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
290
291 // We *should* get an array with just our $group_1 since this is the only group
292 // that we have included.
293 // $group_2 was only used to exclude people.
294 // $group_3 has nothing to do with this mailing and should not be there.
295 $this->assertNotEmpty($groups, "We should have received an array.");
296 $this->assertEquals([$group_1], array_keys($groups),
297 "We should have received an array with our group 1 in it.");
298 }
299
300 }