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 * This class tests the deprecated code that we are moving
27 * away from supporting.
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.
34 * MailingSystemTest is the counterpart to FlexMailerSystemTest.
38 * @see \Civi\FlexMailer\FlexMailerSystemTest
40 class CRM_Mailing_MailingSystemTest
extends CRM_Mailing_BaseMailingSystemTest
{
45 * Set up the deprecated bao support.
47 public function setUp(): void
{
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']);
53 $hooks = \CRM_Utils_Hook
::singleton();
54 $hooks->setHook('civicrm_alterMailParams',
55 [$this, 'hook_alterMailParams']);
56 error_reporting(E_ALL
&& !E_USER_DEPRECATED
);
60 * @see CRM_Utils_Hook::alterMailParams
62 public function hook_alterMailParams(&$params, $context = NULL): void
{
63 $this->counts
['hook_alterMailParams'] = 1;
64 $this->assertEquals('civimail', $context);
70 public function tearDown(): void
{
73 CRM_Core_I18n_Schema
::makeSinglelingual('en_US');
76 $this->assertNotEmpty($this->counts
['hook_alterMailParams']);
80 * Test legacy mailer preview functionality.
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);
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'] = [
92 'contact_id' => $contactID,
94 $params['options']['force_rollback'] = 1;
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;reset=1&jid=&qid=&h=\w*">!', $previewResult['values']['body_html']);
100 $this->assertStringNotContainsString("http://http://", $previewResult['values']['body_html']);
103 // ---- Boilerplate ----
105 // The remainder of this class contains dummy stubs which make it easier to
106 // work with the tests in an IDE.
109 * Generate a fully-formatted mailing (with body_html content).
111 * @dataProvider urlTrackingExamples
113 public function testUrlTracking(
119 parent
::testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params);
122 public function testBasicHeaders() {
123 parent
::testBasicHeaders();
126 public function testText() {
130 public function testHtmlWithOpenTracking() {
131 parent
::testHtmlWithOpenTracking();
134 public function testHtmlWithOpenAndUrlTracking() {
135 parent
::testHtmlWithOpenAndUrlTracking();
139 * Test to check Activity being created on mailing Job.
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(),
150 $this->callAPISuccessGetCount('activity', [
151 'activity_type_id' => 'Bulk Email',
152 'status_id' => 'Completed',
153 'subject' => $subject,
158 * Data provider for testGitLabIssue1108
160 * First we run it without multiLingual mode, then with.
162 * This is because we test table names, which may have been translated in a
163 * multiLingual context.
166 public function multiLingual() {
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.
175 * - also, an INNER JOIN on the group table hid the mailing-based
176 * mailing_group records.
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.
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.
186 * @dataProvider multiLingual
189 public function testGitLabIssue1108($isMultiLingual) {
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');
201 $max_group_id = CRM_Core_DAO
::singleValueQuery("SELECT MAX(id) FROM civicrm_group");
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");
210 $max_mailing_id = $_;
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',
220 $this->createContactsInGroup(2, $group_1);
222 // Also _mut is private to the parent, so we have to make our own:
223 $mut = new CiviMailUtils($this, TRUE);
225 // Create initial mailing to the group.
227 'name' => 'Issue 1108: mailing 1',
228 'subject' => 'Issue 1108: mailing 1',
230 'groups' => ['include' => [$group_1]],
231 'scheduled_date' => 'now',
232 'body_text' => 'Please just {action.unsubscribe}',
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));
240 $allMessages = $mut->getAllMessages('ezc');
241 // There are exactly two contacts produced by setUp().
242 $this->assertEquals(2, count($allMessages));
244 // We need a new group
245 $group_2 = $this->groupCreate([
246 'name' => 'Test Group 1108.2',
247 'title' => 'Test Group 1108.2',
250 // Now create the 2nd mailing to the recipients of the first,
251 // excluding our new albeit empty group.
253 'name' => 'Issue 1108: mailing 2',
254 'subject' => 'Issue 1108: mailing 2',
256 'mailings' => ['include' => [$mailing_1['id']]],
257 'groups' => ['exclude' => [$group_2]],
258 'scheduled_date' => 'now',
259 'body_text' => 'Please just {action.unsubscribeUrl}',
261 $this->callAPISuccess('mailing', 'create', $mailingParams);
262 $_ = $this->callAPISuccess('job', 'process_mailing', array('runInNonProductionEnvironment' => TRUE));
264 $allMessages = $mut->getAllMessages('ezc');
265 // We should have 2+2 messages sent by the mail system now.
266 $this->assertEquals(4, count($allMessages));
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
,
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',
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';
289 while ($gcQuery->fetch()) {
290 $this->callAPISuccess('group_contact', 'create',
291 ['group_id' => $group_3, 'contact_id' => $gcQuery->contact_id
, 'status' => 'Added']);
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.
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();
309 $hooks = \CRM_Utils_Hook
::singleton();
311 $hooks->setHook('civicrm_unsubscribeGroups',
312 function ($op, $mailingId, $contactId, &$groups, &$baseGroups) use (&$found) {
313 $found['groups'] = $groups;
314 $found['baseGroups'] = $baseGroups;
317 // Now test unsubscribe groups.
318 $groups = CRM_Mailing_Event_BAO_Unsubscribe
::unsub_from_mailing(
325 // We expect that our group_1 was found.
326 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
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.");
336 if ($isMultiLingual) {
338 $dbLocale = '_fr_FR';
339 // Now test unsubscribe groups.
340 $groups = CRM_Mailing_Event_BAO_Unsubscribe
::unsub_from_mailing(
347 // We expect that our group_1 was found.
348 $this->assertEquals(['groups' => [$group_1], 'baseGroups' => []], $found);
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.");
358 $dbLocale = '_en_US';