MDL-52126 forum: Verify forum mailout contents
[moodle.git] / mod / forum / tests / mail_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * The forum module mail generation tests.
19  *
20  * @package    mod_forum
21  * @category   external
22  * @copyright  2013 Andrew Nicols
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
30 class mod_forum_mail_testcase extends advanced_testcase {
32     protected $helper;
34     public function setUp() {
35         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
36         // tests using these functions.
37         \mod_forum\subscriptions::reset_forum_cache();
38         \mod_forum\subscriptions::reset_discussion_cache();
40         global $CFG;
41         require_once($CFG->dirroot . '/mod/forum/lib.php');
43         $helper = new stdClass();
45         // Messaging is not compatible with transactions...
46         $this->preventResetByRollback();
48         // Catch all messages.
49         $helper->messagesink = $this->redirectMessages();
50         $helper->mailsink = $this->redirectEmails();
52         // Confirm that we have an empty message sink so far.
53         $messages = $helper->messagesink->get_messages();
54         $this->assertEquals(0, count($messages));
56         $messages = $helper->mailsink->get_messages();
57         $this->assertEquals(0, count($messages));
59         // Forcibly reduce the maxeditingtime to a second in the past to
60         // ensure that messages are sent out.
61         $CFG->maxeditingtime = -1;
63         $this->helper = $helper;
64     }
66     public function tearDown() {
67         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
68         // tests using these functions.
69         \mod_forum\subscriptions::reset_forum_cache();
71         $this->helper->messagesink->clear();
72         $this->helper->messagesink->close();
74         $this->helper->mailsink->clear();
75         $this->helper->mailsink->close();
76     }
78     /**
79      * Perform message inbound setup for the mod_forum reply handler.
80      */
81     protected function helper_spoof_message_inbound_setup() {
82         global $CFG, $DB;
83         // Setup the default Inbound Message mailbox settings.
84         $CFG->messageinbound_domain = 'example.com';
85         $CFG->messageinbound_enabled = true;
87         // Must be no longer than 15 characters.
88         $CFG->messageinbound_mailbox = 'moodlemoodle123';
90         $record = $DB->get_record('messageinbound_handlers', array('classname' => '\mod_forum\message\inbound\reply_handler'));
91         $record->enabled = true;
92         $record->id = $DB->update_record('messageinbound_handlers', $record);
93     }
95     /**
96      * Helper to create the required number of users in the specified
97      * course.
98      * Users are enrolled as students.
99      *
100      * @param stdClass $course The course object
101      * @param integer $count The number of users to create
102      * @return array The users created
103      */
104     protected function helper_create_users($course, $count) {
105         $users = array();
107         for ($i = 0; $i < $count; $i++) {
108             $user = $this->getDataGenerator()->create_user();
109             $this->getDataGenerator()->enrol_user($user->id, $course->id);
110             $users[] = $user;
111         }
113         return $users;
114     }
116     /**
117      * Create a new discussion and post within the specified forum, as the
118      * specified author.
119      *
120      * @param stdClass $forum The forum to post in
121      * @param stdClass $author The author to post as
122      * @param array $fields any other fields in discussion (name, message, messageformat, ...)
123      * @param array An array containing the discussion object, and the post object
124      */
125     protected function helper_post_to_forum($forum, $author, $fields = array()) {
126         global $DB;
127         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
129         // Create a discussion in the forum, and then add a post to that discussion.
130         $record = (object)$fields;
131         $record->course = $forum->course;
132         $record->userid = $author->id;
133         $record->forum = $forum->id;
134         $discussion = $generator->create_discussion($record);
136         // Retrieve the post which was created by create_discussion.
137         $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
139         return array($discussion, $post);
140     }
142     /**
143      * Update the post time for the specified post by $factor.
144      *
145      * @param stdClass $post The post to update
146      * @param int $factor The amount to update by
147      */
148     protected function helper_update_post_time($post, $factor) {
149         global $DB;
151         // Update the post to have a created in the past.
152         $DB->set_field('forum_posts', 'created', $post->created + $factor, array('id' => $post->id));
153     }
155     /**
156      * Update the subscription time for the specified user/discussion by $factor.
157      *
158      * @param stdClass $user The user to update
159      * @param stdClass $discussion The discussion to update for this user
160      * @param int $factor The amount to update by
161      */
162     protected function helper_update_subscription_time($user, $discussion, $factor) {
163         global $DB;
165         $sub = $DB->get_record('forum_discussion_subs', array('userid' => $user->id, 'discussion' => $discussion->id));
167         // Update the subscription to have a preference in the past.
168         $DB->set_field('forum_discussion_subs', 'preference', $sub->preference + $factor, array('id' => $sub->id));
169     }
171     /**
172      * Create a new post within an existing discussion, as the specified author.
173      *
174      * @param stdClass $forum The forum to post in
175      * @param stdClass $discussion The discussion to post in
176      * @param stdClass $author The author to post as
177      * @return stdClass The forum post
178      */
179     protected function helper_post_to_discussion($forum, $discussion, $author) {
180         global $DB;
182         $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
184         // Add a post to the discussion.
185         $record = new stdClass();
186         $record->course = $forum->course;
187         $record->userid = $author->id;
188         $record->forum = $forum->id;
189         $record->discussion = $discussion->id;
190         $record->mailnow = 1;
192         $post = $generator->create_post($record);
194         return $post;
195     }
197     /**
198      * Run the forum cron, and check that the specified post was sent the
199      * specified number of times.
200      *
201      * @param stdClass $post The forum post object
202      * @param integer $expected The number of times that the post should have been sent
203      * @return array An array of the messages caught by the message sink
204      */
205     protected function helper_run_cron_check_count($post, $expected) {
207         // Clear the sinks before running cron.
208         $this->helper->messagesink->clear();
209         $this->helper->mailsink->clear();
211         // Cron daily uses mtrace, turn on buffering to silence output.
212         $this->expectOutputRegex("/{$expected} users were sent post {$post->id}, '{$post->subject}'/");
213         forum_cron();
215         // Now check the results in the message sink.
216         $messages = $this->helper->messagesink->get_messages();
218         // There should be the expected number of messages.
219         $this->assertEquals($expected, count($messages));
221         return $messages;
222     }
224     /**
225      * Run the forum cron, and check that the specified posts were sent the
226      * specified number of times.
227      *
228      * @param stdClass $post The forum post object
229      * @param integer $expected The number of times that the post should have been sent
230      * @return array An array of the messages caught by the message sink
231      */
232     protected function helper_run_cron_check_counts($posts, $expected) {
234         // Clear the sinks before running cron.
235         $this->helper->messagesink->clear();
236         $this->helper->mailsink->clear();
238         // Cron daily uses mtrace, turn on buffering to silence output.
239         foreach ($posts as $post) {
240             $this->expectOutputRegex("/{$post['count']} users were sent post {$post['id']}, '{$post['subject']}'/");
241         }
242         forum_cron();
244         // Now check the results in the message sink.
245         $messages = $this->helper->messagesink->get_messages();
247         // There should be the expected number of messages.
248         $this->assertEquals($expected, count($messages));
250         return $messages;
251     }
253     public function test_forced_subscription() {
254         $this->resetAfterTest(true);
256         // Create a course, with a forum.
257         $course = $this->getDataGenerator()->create_course();
259         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
260         $forum = $this->getDataGenerator()->create_module('forum', $options);
262         // Create two users enrolled in the course as students.
263         list($author, $recipient) = $this->helper_create_users($course, 2);
265         // Post a discussion to the forum.
266         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
268         // We expect both users to receive this post.
269         $expected = 2;
271         // Run cron and check that the expected number of users received the notification.
272         $messages = $this->helper_run_cron_check_count($post, $expected);
274         $seenauthor = false;
275         $seenrecipient = false;
276         foreach ($messages as $message) {
277             // They should both be from our user.
278             $this->assertEquals($author->id, $message->useridfrom);
280             if ($message->useridto == $author->id) {
281                 $seenauthor = true;
282             } else if ($message->useridto = $recipient->id) {
283                 $seenrecipient = true;
284             }
285         }
287         // Check we saw messages for both users.
288         $this->assertTrue($seenauthor);
289         $this->assertTrue($seenrecipient);
290     }
292     public function test_subscription_disabled() {
293         global $DB;
295         $this->resetAfterTest(true);
297         // Create a course, with a forum.
298         $course = $this->getDataGenerator()->create_course();
300         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE);
301         $forum = $this->getDataGenerator()->create_module('forum', $options);
303         // Create two users enrolled in the course as students.
304         list($author, $recipient) = $this->helper_create_users($course, 2);
306         // Post a discussion to the forum.
307         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
309         // We expect both users to receive this post.
310         $expected = 0;
312         // Run cron and check that the expected number of users received the notification.
313         $messages = $this->helper_run_cron_check_count($post, $expected);
315         // A user with the manageactivities capability within the course can subscribe.
316         $expected = 1;
317         $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
318         assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleids['student'], context_course::instance($course->id));
319         \mod_forum\subscriptions::subscribe_user($recipient->id, $forum);
321         $this->assertEquals($expected, $DB->count_records('forum_subscriptions', array(
322             'userid'        => $recipient->id,
323             'forum'         => $forum->id,
324         )));
326         // Run cron and check that the expected number of users received the notification.
327         list($discussion, $post) = $this->helper_post_to_forum($forum, $recipient);
328         $messages = $this->helper_run_cron_check_count($post, $expected);
330         // Unsubscribe the user again.
331         \mod_forum\subscriptions::unsubscribe_user($recipient->id, $forum);
333         $expected = 0;
334         $this->assertEquals($expected, $DB->count_records('forum_subscriptions', array(
335             'userid'        => $recipient->id,
336             'forum'         => $forum->id,
337         )));
339         // Run cron and check that the expected number of users received the notification.
340         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
341         $messages = $this->helper_run_cron_check_count($post, $expected);
343         // Subscribe the user to the discussion.
344         \mod_forum\subscriptions::subscribe_user_to_discussion($recipient->id, $discussion);
345         $this->helper_update_subscription_time($recipient, $discussion, -60);
347         $reply = $this->helper_post_to_discussion($forum, $discussion, $author);
348         $this->helper_update_post_time($reply, -30);
350         $messages = $this->helper_run_cron_check_count($reply, $expected);
351     }
353     public function test_automatic() {
354         $this->resetAfterTest(true);
356         // Create a course, with a forum.
357         $course = $this->getDataGenerator()->create_course();
359         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE);
360         $forum = $this->getDataGenerator()->create_module('forum', $options);
362         // Create two users enrolled in the course as students.
363         list($author, $recipient) = $this->helper_create_users($course, 2);
365         // Post a discussion to the forum.
366         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
368         // We expect both users to receive this post.
369         $expected = 2;
371         // Run cron and check that the expected number of users received the notification.
372         $messages = $this->helper_run_cron_check_count($post, $expected);
374         $seenauthor = false;
375         $seenrecipient = false;
376         foreach ($messages as $message) {
377             // They should both be from our user.
378             $this->assertEquals($author->id, $message->useridfrom);
380             if ($message->useridto == $author->id) {
381                 $seenauthor = true;
382             } else if ($message->useridto = $recipient->id) {
383                 $seenrecipient = true;
384             }
385         }
387         // Check we saw messages for both users.
388         $this->assertTrue($seenauthor);
389         $this->assertTrue($seenrecipient);
390     }
392     public function test_optional() {
393         $this->resetAfterTest(true);
395         // Create a course, with a forum.
396         $course = $this->getDataGenerator()->create_course();
398         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE);
399         $forum = $this->getDataGenerator()->create_module('forum', $options);
401         // Create two users enrolled in the course as students.
402         list($author, $recipient) = $this->helper_create_users($course, 2);
404         // Post a discussion to the forum.
405         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
407         // We expect both users to receive this post.
408         $expected = 0;
410         // Run cron and check that the expected number of users received the notification.
411         $messages = $this->helper_run_cron_check_count($post, $expected);
412     }
414     public function test_automatic_with_unsubscribed_user() {
415         $this->resetAfterTest(true);
417         // Create a course, with a forum.
418         $course = $this->getDataGenerator()->create_course();
420         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE);
421         $forum = $this->getDataGenerator()->create_module('forum', $options);
423         // Create two users enrolled in the course as students.
424         list($author, $recipient) = $this->helper_create_users($course, 2);
426         // Unsubscribe the 'author' user from the forum.
427         \mod_forum\subscriptions::unsubscribe_user($author->id, $forum);
429         // Post a discussion to the forum.
430         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
432         // We expect only one user to receive this post.
433         $expected = 1;
435         // Run cron and check that the expected number of users received the notification.
436         $messages = $this->helper_run_cron_check_count($post, $expected);
438         $seenauthor = false;
439         $seenrecipient = false;
440         foreach ($messages as $message) {
441             // They should both be from our user.
442             $this->assertEquals($author->id, $message->useridfrom);
444             if ($message->useridto == $author->id) {
445                 $seenauthor = true;
446             } else if ($message->useridto = $recipient->id) {
447                 $seenrecipient = true;
448             }
449         }
451         // Check we only saw one user.
452         $this->assertFalse($seenauthor);
453         $this->assertTrue($seenrecipient);
454     }
456     public function test_optional_with_subscribed_user() {
457         $this->resetAfterTest(true);
459         // Create a course, with a forum.
460         $course = $this->getDataGenerator()->create_course();
462         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE);
463         $forum = $this->getDataGenerator()->create_module('forum', $options);
465         // Create two users enrolled in the course as students.
466         list($author, $recipient) = $this->helper_create_users($course, 2);
468         // Subscribe the 'recipient' user from the forum.
469         \mod_forum\subscriptions::subscribe_user($recipient->id, $forum);
471         // Post a discussion to the forum.
472         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
474         // We expect only one user to receive this post.
475         $expected = 1;
477         // Run cron and check that the expected number of users received the notification.
478         $messages = $this->helper_run_cron_check_count($post, $expected);
480         $seenauthor = false;
481         $seenrecipient = false;
482         foreach ($messages as $message) {
483             // They should both be from our user.
484             $this->assertEquals($author->id, $message->useridfrom);
486             if ($message->useridto == $author->id) {
487                 $seenauthor = true;
488             } else if ($message->useridto = $recipient->id) {
489                 $seenrecipient = true;
490             }
491         }
493         // Check we only saw one user.
494         $this->assertFalse($seenauthor);
495         $this->assertTrue($seenrecipient);
496     }
498     public function test_automatic_with_unsubscribed_discussion() {
499         $this->resetAfterTest(true);
501         // Create a course, with a forum.
502         $course = $this->getDataGenerator()->create_course();
504         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE);
505         $forum = $this->getDataGenerator()->create_module('forum', $options);
507         // Create two users enrolled in the course as students.
508         list($author, $recipient) = $this->helper_create_users($course, 2);
510         // Post a discussion to the forum.
511         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
513         // Unsubscribe the 'author' user from the discussion.
514         \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion);
516         $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id));
517         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($recipient->id, $forum, $discussion->id));
519         // We expect only one user to receive this post.
520         $expected = 1;
522         // Run cron and check that the expected number of users received the notification.
523         $messages = $this->helper_run_cron_check_count($post, $expected);
525         $seenauthor = false;
526         $seenrecipient = false;
527         foreach ($messages as $message) {
528             // They should both be from our user.
529             $this->assertEquals($author->id, $message->useridfrom);
531             if ($message->useridto == $author->id) {
532                 $seenauthor = true;
533             } else if ($message->useridto = $recipient->id) {
534                 $seenrecipient = true;
535             }
536         }
538         // Check we only saw one user.
539         $this->assertFalse($seenauthor);
540         $this->assertTrue($seenrecipient);
541     }
543     public function test_optional_with_subscribed_discussion() {
544         $this->resetAfterTest(true);
546         // Create a course, with a forum.
547         $course = $this->getDataGenerator()->create_course();
549         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE);
550         $forum = $this->getDataGenerator()->create_module('forum', $options);
552         // Create two users enrolled in the course as students.
553         list($author, $recipient) = $this->helper_create_users($course, 2);
555         // Post a discussion to the forum.
556         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
557         $this->helper_update_post_time($post, -90);
559         // Subscribe the 'recipient' user to the discussion.
560         \mod_forum\subscriptions::subscribe_user_to_discussion($recipient->id, $discussion);
561         $this->helper_update_subscription_time($recipient, $discussion, -60);
563         // Initially we don't expect any user to receive this post as you cannot subscribe to a discussion until after
564         // you have read it.
565         $expected = 0;
567         // Run cron and check that the expected number of users received the notification.
568         $messages = $this->helper_run_cron_check_count($post, $expected);
570         // Have a user reply to the discussion.
571         $reply = $this->helper_post_to_discussion($forum, $discussion, $author);
572         $this->helper_update_post_time($reply, -30);
574         // We expect only one user to receive this post.
575         $expected = 1;
577         // Run cron and check that the expected number of users received the notification.
578         $messages = $this->helper_run_cron_check_count($reply, $expected);
580         $seenauthor = false;
581         $seenrecipient = false;
582         foreach ($messages as $message) {
583             // They should both be from our user.
584             $this->assertEquals($author->id, $message->useridfrom);
586             if ($message->useridto == $author->id) {
587                 $seenauthor = true;
588             } else if ($message->useridto = $recipient->id) {
589                 $seenrecipient = true;
590             }
591         }
593         // Check we only saw one user.
594         $this->assertFalse($seenauthor);
595         $this->assertTrue($seenrecipient);
596     }
598     public function test_automatic_with_subscribed_discussion_in_unsubscribed_forum() {
599         $this->resetAfterTest(true);
601         // Create a course, with a forum.
602         $course = $this->getDataGenerator()->create_course();
604         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE);
605         $forum = $this->getDataGenerator()->create_module('forum', $options);
607         // Create two users enrolled in the course as students.
608         list($author, $recipient) = $this->helper_create_users($course, 2);
610         // Post a discussion to the forum.
611         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
612         $this->helper_update_post_time($post, -90);
614         // Unsubscribe the 'author' user from the forum.
615         \mod_forum\subscriptions::unsubscribe_user($author->id, $forum);
617         // Then re-subscribe them to the discussion.
618         \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion);
619         $this->helper_update_subscription_time($author, $discussion, -60);
621         // We expect just the user subscribed to the forum to receive this post at the moment as the discussion
622         // subscription time is after the post time.
623         $expected = 1;
625         // Run cron and check that the expected number of users received the notification.
626         $messages = $this->helper_run_cron_check_count($post, $expected);
628         $seenauthor = false;
629         $seenrecipient = false;
630         foreach ($messages as $message) {
631             // They should both be from our user.
632             $this->assertEquals($author->id, $message->useridfrom);
634             if ($message->useridto == $author->id) {
635                 $seenauthor = true;
636             } else if ($message->useridto = $recipient->id) {
637                 $seenrecipient = true;
638             }
639         }
641         // Check we only saw one user.
642         $this->assertFalse($seenauthor);
643         $this->assertTrue($seenrecipient);
645         // Now post a reply to the original post.
646         $reply = $this->helper_post_to_discussion($forum, $discussion, $author);
647         $this->helper_update_post_time($reply, -30);
649         // We expect two users to receive this post.
650         $expected = 2;
652         // Run cron and check that the expected number of users received the notification.
653         $messages = $this->helper_run_cron_check_count($reply, $expected);
655         $seenauthor = false;
656         $seenrecipient = false;
657         foreach ($messages as $message) {
658             // They should both be from our user.
659             $this->assertEquals($author->id, $message->useridfrom);
661             if ($message->useridto == $author->id) {
662                 $seenauthor = true;
663             } else if ($message->useridto = $recipient->id) {
664                 $seenrecipient = true;
665             }
666         }
668         // Check we saw both users.
669         $this->assertTrue($seenauthor);
670         $this->assertTrue($seenrecipient);
671     }
673     public function test_optional_with_unsubscribed_discussion_in_subscribed_forum() {
674         $this->resetAfterTest(true);
676         // Create a course, with a forum.
677         $course = $this->getDataGenerator()->create_course();
679         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE);
680         $forum = $this->getDataGenerator()->create_module('forum', $options);
682         // Create two users enrolled in the course as students.
683         list($author, $recipient) = $this->helper_create_users($course, 2);
685         // Post a discussion to the forum.
686         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
688         // Unsubscribe the 'recipient' user from the discussion.
689         \mod_forum\subscriptions::subscribe_user($recipient->id, $forum);
691         // Then unsubscribe them from the discussion.
692         \mod_forum\subscriptions::unsubscribe_user_from_discussion($recipient->id, $discussion);
694         // We don't expect any users to receive this post.
695         $expected = 0;
697         // Run cron and check that the expected number of users received the notification.
698         $messages = $this->helper_run_cron_check_count($post, $expected);
699     }
701     /**
702      * Test that a user unsubscribed from a forum who has subscribed to a discussion, only receives posts made after
703      * they subscribed to the discussion.
704      */
705     public function test_forum_discussion_subscription_forum_unsubscribed_discussion_subscribed_after_post() {
706         $this->resetAfterTest(true);
708         // Create a course, with a forum.
709         $course = $this->getDataGenerator()->create_course();
711         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE);
712         $forum = $this->getDataGenerator()->create_module('forum', $options);
714         $expectedmessages = array();
716         // Create a user enrolled in the course as a student.
717         list($author) = $this->helper_create_users($course, 1);
719         // Post a discussion to the forum.
720         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
721         $this->helper_update_post_time($post, -90);
723         $expectedmessages[] = array(
724             'id' => $post->id,
725             'subject' => $post->subject,
726             'count' => 0,
727         );
729         // Then subscribe the user to the discussion.
730         $this->assertTrue(\mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion));
731         $this->helper_update_subscription_time($author, $discussion, -60);
733         // Then post a reply to the first discussion.
734         $reply = $this->helper_post_to_discussion($forum, $discussion, $author);
735         $this->helper_update_post_time($reply, -30);
737         $expectedmessages[] = array(
738             'id' => $reply->id,
739             'subject' => $reply->subject,
740             'count' => 1,
741         );
743         $expectedcount = 1;
745         // Run cron and check that the expected number of users received the notification.
746         $messages = $this->helper_run_cron_check_counts($expectedmessages, $expectedcount);
747     }
749     public function test_forum_message_inbound_multiple_posts() {
750         $this->resetAfterTest(true);
752         // Create a course, with a forum.
753         $course = $this->getDataGenerator()->create_course();
754         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
755         $forum = $this->getDataGenerator()->create_module('forum', $options);
757         // Create a user enrolled in the course as a student.
758         list($author) = $this->helper_create_users($course, 1);
760         $expectedmessages = array();
762         // Post a discussion to the forum.
763         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
764         $this->helper_update_post_time($post, -90);
766         $expectedmessages[] = array(
767             'id' => $post->id,
768             'subject' => $post->subject,
769             'count' => 0,
770         );
772         // Then post a reply to the first discussion.
773         $reply = $this->helper_post_to_discussion($forum, $discussion, $author);
774         $this->helper_update_post_time($reply, -60);
776         $expectedmessages[] = array(
777             'id' => $reply->id,
778             'subject' => $reply->subject,
779             'count' => 1,
780         );
782         $expectedcount = 2;
784         // Ensure that messageinbound is enabled and configured for the forum handler.
785         $this->helper_spoof_message_inbound_setup();
787         $author->emailstop = '0';
788         set_user_preference('message_provider_mod_forum_posts_loggedoff', 'email', $author);
789         set_user_preference('message_provider_mod_forum_posts_loggedin', 'email', $author);
791         // Run cron and check that the expected number of users received the notification.
792         // Clear the mailsink, and close the messagesink.
793         $this->helper->mailsink->clear();
794         $this->helper->messagesink->close();
796         // Cron daily uses mtrace, turn on buffering to silence output.
797         foreach ($expectedmessages as $post) {
798             $this->expectOutputRegex("/{$post['count']} users were sent post {$post['id']}, '{$post['subject']}'/");
799         }
801         forum_cron();
802         $messages = $this->helper->mailsink->get_messages();
804         // There should be the expected number of messages.
805         $this->assertEquals($expectedcount, count($messages));
807         foreach ($messages as $message) {
808             $this->assertRegExp('/Reply-To: moodlemoodle123\+[^@]*@example.com/', $message->header);
809         }
810     }
812     public function test_long_subject() {
813         $this->resetAfterTest(true);
815         // Create a course, with a forum.
816         $course = $this->getDataGenerator()->create_course();
818         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
819         $forum = $this->getDataGenerator()->create_module('forum', $options);
821         // Create a user enrolled in the course as student.
822         list($author) = $this->helper_create_users($course, 1);
824         // Post a discussion to the forum.
825         $subject = 'This is the very long forum post subject that somebody was very kind of leaving, it is intended to check if long subject comes in mail correctly. Thank you.';
826         $a = (object)array('courseshortname' => $course->shortname, 'forumname' => $forum->name, 'subject' => $subject);
827         $expectedsubject = get_string('postmailsubject', 'forum', $a);
828         list($discussion, $post) = $this->helper_post_to_forum($forum, $author, array('name' => $subject));
830         // Run cron and check that the expected number of users received the notification.
831         $messages = $this->helper_run_cron_check_count($post, 1);
832         $message = reset($messages);
833         $this->assertEquals($author->id, $message->useridfrom);
834         $this->assertEquals($expectedsubject, $message->subject);
835     }
837     /**
838      * dataProvider for test_forum_post_email_templates().
839      */
840     public function forum_post_email_templates_provider() {
841         // Base information, we'll build variations based on it.
842         $base = array(
843             'user' => array('firstname' => 'Love', 'lastname' => 'Moodle', 'mailformat' => 0, 'maildigest' => 0),
844             'course' => array('shortname' => '101', 'fullname' => 'Moodle 101'),
845             'forums' => array(
846                 array(
847                     'name' => 'Moodle Forum',
848                     'forumposts' => array(
849                         array(
850                             'name' => 'Hello Moodle',
851                             'message' => 'Welcome to Moodle',
852                             'messageformat' => FORMAT_MOODLE,
853                             'attachments' => array(
854                                 array(
855                                     'filename' => 'example.txt',
856                                     'filecontents' => 'Basic information about the course'
857                                 ),
858                             ),
859                         ),
860                     ),
861                 ),
862             ),
863             'expectations' => array(
864                 array(
865                     'subject' => '.*101.*Hello',
866                     'contents' => array(
867                         '~{$a',
868                         '~&(amp|lt|gt|quot|\#039);(?!course)',
869                         'Attachment example.txt:\n' .
870                             'http://www.example.com/moodle/pluginfile.php/\d*/mod_forum/attachment/\d*/example.txt\n',
871                         'Hello Moodle', 'Moodle Forum', 'Welcome.*Moodle', 'Love Moodle', '1\d1'
872                     ),
873                 ),
874             ),
875         );
877         // Build the text cases.
878         $textcases = array('Text mail without ampersands, quotes or lt/gt' => array('data' => $base));
880         // Single and double quotes everywhere.
881         $newcase = $base;
882         $newcase['user']['lastname'] = 'Moodle\'';
883         // $newcase['user']['lastname'] = 'Moodle\'"'; // TODO: This breaks badly. See MDL-52136.
884         $newcase['course']['shortname'] = '101\'';
885         // $newcase['course']['shortname'] = '101\'"'; // TODO: This breaks badly. See MDL-52136.
886         $newcase['forums'][0]['name'] = 'Moodle Forum\'"';
887         $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle\'"';
888         $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle\'"';
889         $newcase['expectations'][0]['contents'] = array(
890             'Attachment example.txt:', '~{\$a', '~&amp;(quot|\#039);', 'Love Moodle\'', '101\'', 'Moodle Forum\'"',
891             'Hello Moodle\'"', 'Welcome to Moodle\'"');
892         $textcases['Text mail with quotes everywhere'] = array('data' => $newcase);
894         // Lt and gt everywhere. This case is completely borked because format_string()
895         // strips tags with $CFG->formatstringstriptags and also escapes < and > (correct
896         // for web presentation but not for text email). See MDL-19829.
897         $newcase = $base;
898         $newcase['user']['lastname'] = 'Moodle>';
899         $newcase['course']['shortname'] = '101>';
900         $newcase['forums'][0]['name'] = 'Moodle Forum>';
901         $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle>';
902         $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle>';
903         $newcase['expectations'][0]['contents'] = array(
904             'Attachment example.txt:', '~{\$a', '~&amp;gt;', 'Love Moodle>', '101&gt;', 'Moodle Forum&gt;',
905             'Hello Moodle&gt;', 'Welcome to Moodle>');
906         $textcases['Text mail with gt and lt everywhere'] = array('data' => $newcase);
908         // Ampersands everywhere. This case is completely borked because format_string()
909         // escapes ampersands (correct for web presentation but not for text email). See MDL-19829.
910         $newcase = $base;
911         $newcase['user']['lastname'] = 'Moodle&';
912         $newcase['course']['shortname'] = '101&';
913         $newcase['forums'][0]['name'] = 'Moodle Forum&';
914         $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle&';
915         $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle&';
916         $newcase['expectations'][0]['contents'] = array(
917             'Attachment example.txt:', '~{\$a', '~&amp;amp;', 'Love Moodle&', '101&amp;', 'Moodle Forum&amp;',
918             'Hello Moodle&amp;', 'Welcome to Moodle&');
919         $textcases['Text mail with ampersands everywhere'] = array('data' => $newcase);
921         // Now the html cases.
922         $htmlcases = array();
924         // New base for html cases, no quotes, lts, gts or ampersands.
925         $htmlbase = $base;
926         $htmlbase['user']['mailformat'] = 1;
927         $htmlbase['expectations'][0]['contents'] = array(
928             '~{\$a',
929             '~&(amp|lt|gt|quot|\#039);(?!course)',
930             '<div class=3D"attachments">( *\n *)?<a href',
931             '<div class=3D"subject">\n.*Hello Moodle', '>Moodle Forum', '>Welcome.*Moodle', '>Love Moodle', '>1\d1');
932         $htmlcases['HTML mail without ampersands, quotes or lt/gt'] = array('data' => $htmlbase);
934         // Single and double quotes, lt and gt, ampersands everywhere.
935         $newcase = $htmlbase;
936         $newcase['user']['lastname'] = 'Moodle\'>&';
937         // $newcase['user']['lastname'] = 'Moodle\'">&'; // TODO: This breaks badly. See MDL-52136.
938         $newcase['course']['shortname'] = '101\'>&';
939         // $newcase['course']['shortname'] = '101\'">&'; // TODO: This breaks badly. See MDL-52136.
940         $newcase['forums'][0]['name'] = 'Moodle Forum\'">&';
941         $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle\'">&';
942         $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle\'">&';
943         $newcase['expectations'][0]['contents'] = array(
944             '~{\$a',
945             '~&amp;(amp|lt|gt|quot|\#039);',
946             '<div class=3D"attachments">( *\n *)?<a href',
947             '<div class=3D"subject">\n.*Hello Moodle\'"&gt;&amp;', '>Moodle Forum\'"&gt;&amp;',
948             '>Welcome.*Moodle\'"&gt;&amp;', '>Love Moodle&\#039;&gt;&amp;', '>1\d1\'&gt;&amp');
949         $htmlcases['HTML mail with quotes, gt, lt and ampersand  everywhere'] = array('data' => $newcase);
951         return $textcases + $htmlcases;
952     }
954     /**
955      * Verify forum emails body using templates to generate the expected results.
956      *
957      * @dataProvider forum_post_email_templates_provider
958      * @param array $data provider samples.
959      */
960     public function test_forum_post_email_templates($data) {
961         global $DB;
963         $this->resetAfterTest();
965         // Create the course, with the specified options.
966         $options = array();
967         foreach ($data['course'] as $option => $value) {
968             $options[$option] = $value;
969         }
970         $course = $this->getDataGenerator()->create_course($options);
972         // Create the user, with the specified options and enrol in the course.
973         $options = array();
974         foreach ($data['user'] as $option => $value) {
975             $options[$option] = $value;
976         }
977         $user = $this->getDataGenerator()->create_user($options);
978         $this->getDataGenerator()->enrol_user($user->id, $course->id);
980         // Create forums, always force susbscribed (for easy), with the specified options.
981         $posts = array();
982         foreach ($data['forums'] as $dataforum) {
983             $forumposts = isset($dataforum['forumposts']) ? $dataforum['forumposts'] : array();
984             unset($dataforum['forumposts']);
985             $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
986             foreach ($dataforum as $option => $value) {
987                 $options[$option] = $value;
988             }
989             $forum = $this->getDataGenerator()->create_module('forum', $options);
991             // Create posts, always for immediate delivery (for easy), with the specified options.
992             foreach ($forumposts as $forumpost) {
993                 $attachments = isset($forumpost['attachments']) ? $forumpost['attachments'] : array();
994                 unset($forumpost['attachments']);
995                 $postoptions = array('course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id,
996                     'mailnow' => 1, 'attachment' => !empty($attachments));
997                 foreach ($forumpost as $option => $value) {
998                     $postoptions[$option] = $value;
999                 }
1000                 list($discussion, $post) = $this->helper_post_to_forum($forum, $user, $postoptions);
1001                 $posts[$post->subject] = $post; // Need this to verify cron output.
1003                 // Add the attachments to the post.
1004                 if ($attachments) {
1005                     $fs = get_file_storage();
1006                     foreach ($attachments as $attachment) {
1007                         $filerecord = array(
1008                             'contextid' => context_module::instance($forum->cmid)->id,
1009                             'component' => 'mod_forum',
1010                             'filearea'  => 'attachment',
1011                             'itemid'    => $post->id,
1012                             'filepath'  => '/',
1013                             'filename'  => $attachment['filename']
1014                         );
1015                         $fs->create_file_from_string($filerecord, $attachment['filecontents']);
1016                     }
1017                     $DB->set_field('forum_posts', 'attachment', '1', array('id' => $post->id));
1018                 }
1019             }
1020         }
1022         // Clear the mailsink and close the messagesink.
1023         // (surely setup should provide us this cleared but...)
1024         $this->helper->mailsink->clear();
1025         $this->helper->messagesink->close();
1027         // Capture and silence cron output, verifying contents.
1028         foreach ($posts as $post) {
1029             $this->expectOutputRegex("/1 users were sent post {$post->id}, '{$post->subject}'/");
1030         }
1031         forum_cron(); // It's really annoying that we have to run cron to test this.
1033         // Get the mails.
1034         $mails = $this->helper->mailsink->get_messages();
1036         // Start testing the expectations.
1037         $expectations = $data['expectations'];
1039         // Assert the number is the expected.
1040         $this->assertSame(count($expectations), count($mails));
1042         // Start processing mails, first localizing its expectations, then checking them.
1043         foreach ($mails as $mail) {
1044             // Find the corresponding expectation.
1045             $foundexpectation = null;
1046             foreach ($expectations as $key => $expectation) {
1047                 // All expectations must have a subject for matching.
1048                 if (!isset($expectation['subject'])) {
1049                     $this->fail('Provider expectation missing mandatory subject');
1050                 }
1051                 if (preg_match('!' . $expectation['subject'] . '!', $mail->subject)) {
1052                     // If we already had found the expectation, there are non-unique subjects. Fail.
1053                     if (isset($foundexpectation)) {
1054                         $this->fail('Multiple expectations found (by subject matching). Please make them unique.');
1055                     }
1056                     $foundexpectation = $expectation;
1057                     unset($expectations[$key]);
1058                 }
1059             }
1060             // Arrived here, we should have found the expectations.
1061             $this->assertNotEmpty($foundexpectation, 'Expectation not found for the mail');
1063             // If we have found the expectation and have contents to match, let's do it.
1064             if (isset($foundexpectation) and isset($foundexpectation['contents'])) {
1065                 if (!is_array($foundexpectation['contents'])) { // Accept both string and array.
1066                     $foundexpectation['contents'] = array($foundexpectation['contents']);
1067                 }
1068                 foreach ($foundexpectation['contents'] as $content) {
1069                     if (strpos($content, '~') !== 0) {
1070                         $this->assertRegexp('#' . $content . '#m', $mail->body);
1071                     } else {
1072                         $this->assertNotRegexp('#' . substr($content, 1) . '#m', $mail->body);
1073                     }
1074                 }
1075             }
1076         }
1077         // Finished, there should not be remaining expectations.
1078         $this->assertCount(0, $expectations);
1079     }