3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * Test that content produced by CiviMail looks the way it's expected.
15 * @package CiviCRM_APIv3
18 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * @version $Id: Job.php 30879 2010-11-22 15:45:55Z shot $
24 * Class CRM_Mailing_MailingSystemTest
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.
31 * MailingSystemTest is the counterpart to FlexMailerSystemTest.
35 * @see \Civi\FlexMailer\FlexMailerSystemTest
37 class CRM_Mailing_MailingSystemTest
extends CRM_Mailing_BaseMailingSystemTest
{
41 public function setUp() {
43 Civi
::settings()->add(['experimentalFlexMailerEngine' => FALSE]);
45 $hooks = \CRM_Utils_Hook
::singleton();
46 $hooks->setHook('civicrm_alterMailParams',
47 [$this, 'hook_alterMailParams']);
51 * @see CRM_Utils_Hook::alterMailParams
53 public function hook_alterMailParams(&$params, $context = NULL) {
54 $this->counts
['hook_alterMailParams'] = 1;
55 $this->assertEquals('civimail', $context);
58 public function tearDown() {
61 CRM_Core_I18n_Schema
::makeSinglelingual('en_US');
64 $this->assertNotEmpty($this->counts
['hook_alterMailParams']);
67 // ---- Boilerplate ----
69 // The remainder of this class contains dummy stubs which make it easier to
70 // work with the tests in an IDE.
73 * Generate a fully-formatted mailing (with body_html content).
75 * @dataProvider urlTrackingExamples
77 public function testUrlTracking(
83 parent
::testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params);
86 public function testBasicHeaders() {
87 parent
::testBasicHeaders();
90 public function testText() {
94 public function testHtmlWithOpenTracking() {
95 parent
::testHtmlWithOpenTracking();
98 public function testHtmlWithOpenAndUrlTracking() {
99 parent
::testHtmlWithOpenAndUrlTracking();
103 * Test to check Activity being created on mailing Job.
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(),
114 $this->callAPISuccessGetCount('activity', [
115 'activity_type_id' => 'Bulk Email',
116 'status_id' => 'Completed',
117 'subject' => $subject,
122 * Data provider for testGitLabIssue1108
124 * First we run it without multiLingual mode, then with.
126 * This is because we test table names, which may have been translated in a
127 * multiLingual context.
130 public function multiLingual() {
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.
139 * - also, an INNER JOIN on the group table hid the mailing-based
140 * mailing_group records.
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.
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.
150 * @dataProvider multiLingual
153 public function testGitLabIssue1108($isMultiLingual) {
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 CRM_Core_I18n_Schema
::addLocale('fr_FR', 'en_US');
165 $max_group_id = CRM_Core_DAO
::singleValueQuery("SELECT MAX(id) FROM civicrm_group");
167 while ($max_mailing_id < $max_group_id +
10) {
168 CRM_Core_Transaction
::create()->run(function($tx) use (&$max_mailing_id) {
169 CRM_Core_DAO
::executeQuery("INSERT INTO civicrm_mailing (name) VALUES ('dummy');");
170 $_ = (int) CRM_Core_DAO
::singleValueQuery("SELECT MAX(id) FROM civicrm_mailing");
171 if ($_ === $max_mailing_id) {
172 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");
174 $max_mailing_id = $_;
179 // Because our parent class marks the _groupID as private, we can't use that :-(
180 $group_1 = $this->groupCreate([
181 'name' => 'Test Group 1108.1',
182 'title' => 'Test Group 1108.1',
184 $this->createContactsInGroup(2, $group_1);
186 // Also _mut is private to the parent, so we have to make our own:
187 $mut = new CiviMailUtils($this, TRUE);
189 // Create initial mailing to the group.
191 'name' => 'Issue 1108: mailing 1',
192 'subject' => 'Issue 1108: mailing 1',
194 'groups' => ['include' => [$group_1]],
195 'scheduled_date' => 'now',
196 'body_text' => 'Please just {action.unsubscribe}',
199 // The following code is exactly the same as runMailingSuccess() except that we store the ID of the mailing.
200 $mailing_1 = $this->callAPISuccess('mailing', 'create', $mailingParams);
201 $mut->assertRecipients(array());
202 $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
204 $allMessages = $mut->getAllMessages('ezc');
205 // There are exactly two contacts produced by setUp().
206 $this->assertEquals(2, count($allMessages));
208 // We need a new group
209 $group_2 = $this->groupCreate([
210 'name' => 'Test Group 1108.2',
211 'title' => 'Test Group 1108.2',
214 // Now create the 2nd mailing to the recipients of the first,
215 // excluding our new albeit empty group.
217 'name' => 'Issue 1108: mailing 2',
218 'subject' => 'Issue 1108: mailing 2',
220 'mailings' => ['include' => [$mailing_1['id']]],
221 'groups' => ['exclude' => [$group_2]],
222 'scheduled_date' => 'now',
223 'body_text' => 'Please just {action.unsubscribeUrl}',
225 $this->callAPISuccess('mailing', 'create', $mailingParams);
226 $_ = $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
228 $allMessages = $mut->getAllMessages('ezc');
229 // We should have 2+2 messages sent by the mail system now.
230 $this->assertEquals(4, count($allMessages));
233 // Now extract the unsubscribe details.
234 $message = end($allMessages);
235 $this->assertTrue($message->body
instanceof ezcMailText
);
236 $this->assertEquals('plain', $message->body
->subType
);
237 $this->assertEquals(1, preg_match(
238 '@mailing/unsubscribe.*jid=(\d+)&qid=(\d+)&h=([0-9a-z]+)@',
239 $message->body
->text
,
243 // Create a group that has nothing to do with this mailing.
244 $group_3 = $this->groupCreate([
245 'name' => 'Test Group 1108.3',
246 'title' => 'Test Group 1108.3',
248 // Add contacts from group 1 to group 3.
249 $gcQuery = new CRM_Contact_BAO_GroupContact();
250 $gcQuery->group_id
= $group_1;
251 $gcQuery->status
= 'Added';
253 while ($gcQuery->fetch()) {
254 $this->callAPISuccess('group_contact', 'create',
255 ['group_id' => $group_3, 'contact_id' => $gcQuery->contact_id
, 'status' => 'Added']);
258 // Part of the issue is caused by the fact that (at time of writing) the
259 // SQL joined the mailing_group table on just the entity_id, assuming it to
260 // be a group, but actually it could be a mailing.
261 // The difficulty in testing this is that because all our IDs are very low
262 // and contiguous the SQL looking for a match for 'mailing 1' does match a
263 // group ID of '1', which is created in this class's parent's setUp().
264 // Strictly speaking we don't know that it has ID 1, but as we can't access _groupID
265 // we'll have to assume that.
267 // So by deleting that group the SQL then matches nothing which is what we
268 // need for this case.
269 $_ = new CRM_Contact_BAO_Group();
273 $hooks = \CRM_Utils_Hook
::singleton();
275 $hooks->setHook('civicrm_unsubscribeGroups',
276 function ($op, $mailingId, $contactId, &$groups, &$baseGroups) use (&$found) {
277 $found['groups'] = $groups;
278 $found['baseGroups'] = $baseGroups;
281 // Now test unsubscribe groups.
282 $groups = CRM_Mailing_Event_BAO_Unsubscribe
::unsub_from_mailing(
289 // We expect that our group_1 was found.
290 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
292 // We *should* get an array with just our $group_1 since this is the only group
293 // that we have included.
294 // $group_2 was only used to exclude people.
295 // $group_3 has nothing to do with this mailing and should not be there.
296 $this->assertNotEmpty($groups, "We should have received an array.");
297 $this->assertEquals([$group_1], array_keys($groups),
298 "We should have received an array with our group 1 in it.");
300 if ($isMultiLingual) {
302 $dbLocale = '_fr_FR';
303 // Now test unsubscribe groups.
304 $groups = CRM_Mailing_Event_BAO_Unsubscribe
::unsub_from_mailing(
311 // We expect that our group_1 was found.
312 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
314 // We *should* get an array with just our $group_1 since this is the only group
315 // that we have included.
316 // $group_2 was only used to exclude people.
317 // $group_3 has nothing to do with this mailing and should not be there.
318 $this->assertNotEmpty($groups, "We should have received an array.");
319 $this->assertEquals([$group_1], array_keys($groups),
320 "We should have received an array with our group 1 in it.");
322 $dbLocale = '_en_US';