Merge branch 'MDL-64284-36_get_component_classes_cache' of https://github.com/tomdick...
[moodle.git] / mod / forum / tests / externallib_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 module forums external functions unit tests
19  *
20  * @package    mod_forum
21  * @category   external
22  * @copyright  2012 Mark Nelson <markn@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
30 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
31 require_once($CFG->dirroot . '/mod/forum/lib.php');
33 class mod_forum_external_testcase extends externallib_advanced_testcase {
35     /**
36      * Tests set up
37      */
38     protected function setUp() {
39         global $CFG;
41         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
42         // tests using these functions.
43         \mod_forum\subscriptions::reset_forum_cache();
45         require_once($CFG->dirroot . '/mod/forum/externallib.php');
46     }
48     public function tearDown() {
49         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
50         // tests using these functions.
51         \mod_forum\subscriptions::reset_forum_cache();
52     }
54     /**
55      * Test get forums
56      */
57     public function test_mod_forum_get_forums_by_courses() {
58         global $USER, $CFG, $DB;
60         $this->resetAfterTest(true);
62         // Create a user.
63         $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
65         // Set to the user.
66         self::setUser($user);
68         // Create courses to add the modules.
69         $course1 = self::getDataGenerator()->create_course();
70         $course2 = self::getDataGenerator()->create_course();
72         // First forum.
73         $record = new stdClass();
74         $record->introformat = FORMAT_HTML;
75         $record->course = $course1->id;
76         $record->trackingtype = FORUM_TRACKING_FORCED;
77         $forum1 = self::getDataGenerator()->create_module('forum', $record);
79         // Second forum.
80         $record = new stdClass();
81         $record->introformat = FORMAT_HTML;
82         $record->course = $course2->id;
83         $record->trackingtype = FORUM_TRACKING_OFF;
84         $forum2 = self::getDataGenerator()->create_module('forum', $record);
85         $forum2->introfiles = [];
87         // Add discussions to the forums.
88         $record = new stdClass();
89         $record->course = $course1->id;
90         $record->userid = $user->id;
91         $record->forum = $forum1->id;
92         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
93         // Expect one discussion.
94         $forum1->numdiscussions = 1;
95         $forum1->cancreatediscussions = true;
96         $forum1->istracked = true;
97         $forum1->unreadpostscount = 0;
98         $forum1->introfiles = [];
100         $record = new stdClass();
101         $record->course = $course2->id;
102         $record->userid = $user->id;
103         $record->forum = $forum2->id;
104         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
105         $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
106         // Expect two discussions.
107         $forum2->numdiscussions = 2;
108         // Default limited role, no create discussion capability enabled.
109         $forum2->cancreatediscussions = false;
110         $forum2->istracked = false;
112         // Check the forum was correctly created.
113         $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2',
114                 array('forum1' => $forum1->id, 'forum2' => $forum2->id)));
116         // Enrol the user in two courses.
117         // DataGenerator->enrol_user automatically sets a role for the user with the permission mod/form:viewdiscussion.
118         $this->getDataGenerator()->enrol_user($user->id, $course1->id, null, 'manual');
119         // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
120         $enrol = enrol_get_plugin('manual');
121         $enrolinstances = enrol_get_instances($course2->id, true);
122         foreach ($enrolinstances as $courseenrolinstance) {
123             if ($courseenrolinstance->enrol == "manual") {
124                 $instance2 = $courseenrolinstance;
125                 break;
126             }
127         }
128         $enrol->enrol_user($instance2, $user->id);
130         // Assign capabilities to view forums for forum 2.
131         $cm2 = get_coursemodule_from_id('forum', $forum2->cmid, 0, false, MUST_EXIST);
132         $context2 = context_module::instance($cm2->id);
133         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
134         $roleid2 = $this->assignUserCapability('mod/forum:viewdiscussion', $context2->id, $newrole);
136         // Create what we expect to be returned when querying the two courses.
137         unset($forum1->displaywordcount);
138         unset($forum2->displaywordcount);
140         $expectedforums = array();
141         $expectedforums[$forum1->id] = (array) $forum1;
142         $expectedforums[$forum2->id] = (array) $forum2;
144         // Call the external function passing course ids.
145         $forums = mod_forum_external::get_forums_by_courses(array($course1->id, $course2->id));
146         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
147         $this->assertCount(2, $forums);
148         foreach ($forums as $forum) {
149             $this->assertEquals($expectedforums[$forum['id']], $forum);
150         }
152         // Call the external function without passing course id.
153         $forums = mod_forum_external::get_forums_by_courses();
154         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
155         $this->assertCount(2, $forums);
156         foreach ($forums as $forum) {
157             $this->assertEquals($expectedforums[$forum['id']], $forum);
158         }
160         // Unenrol user from second course and alter expected forums.
161         $enrol->unenrol_user($instance2, $user->id);
162         unset($expectedforums[$forum2->id]);
164         // Call the external function without passing course id.
165         $forums = mod_forum_external::get_forums_by_courses();
166         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
167         $this->assertCount(1, $forums);
168         $this->assertEquals($expectedforums[$forum1->id], $forums[0]);
169         $this->assertTrue($forums[0]['cancreatediscussions']);
171         // Change the type of the forum, the user shouldn't be able to add discussions.
172         $DB->set_field('forum', 'type', 'news', array('id' => $forum1->id));
173         $forums = mod_forum_external::get_forums_by_courses();
174         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
175         $this->assertFalse($forums[0]['cancreatediscussions']);
177         // Call for the second course we unenrolled the user from.
178         $forums = mod_forum_external::get_forums_by_courses(array($course2->id));
179         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
180         $this->assertCount(0, $forums);
181     }
183     /**
184      * Test get forum posts
185      */
186     public function test_mod_forum_get_forum_discussion_posts() {
187         global $CFG, $PAGE;
189         $this->resetAfterTest(true);
191         // Set the CFG variable to allow track forums.
192         $CFG->forum_trackreadposts = true;
194         // Create a user who can track forums.
195         $record = new stdClass();
196         $record->trackforums = true;
197         $user1 = self::getDataGenerator()->create_user($record);
198         // Create a bunch of other users to post.
199         $user2 = self::getDataGenerator()->create_user();
200         $user3 = self::getDataGenerator()->create_user();
202         // Set the first created user to the test user.
203         self::setUser($user1);
205         // Create course to add the module.
206         $course1 = self::getDataGenerator()->create_course();
208         // Forum with tracking off.
209         $record = new stdClass();
210         $record->course = $course1->id;
211         $record->trackingtype = FORUM_TRACKING_OFF;
212         $forum1 = self::getDataGenerator()->create_module('forum', $record);
213         $forum1context = context_module::instance($forum1->cmid);
215         // Forum with tracking enabled.
216         $record = new stdClass();
217         $record->course = $course1->id;
218         $forum2 = self::getDataGenerator()->create_module('forum', $record);
219         $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
220         $forum2context = context_module::instance($forum2->cmid);
222         // Add discussions to the forums.
223         $record = new stdClass();
224         $record->course = $course1->id;
225         $record->userid = $user1->id;
226         $record->forum = $forum1->id;
227         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
229         $record = new stdClass();
230         $record->course = $course1->id;
231         $record->userid = $user2->id;
232         $record->forum = $forum1->id;
233         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
235         $record = new stdClass();
236         $record->course = $course1->id;
237         $record->userid = $user2->id;
238         $record->forum = $forum2->id;
239         $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
241         // Add 2 replies to the discussion 1 from different users.
242         $record = new stdClass();
243         $record->discussion = $discussion1->id;
244         $record->parent = $discussion1->firstpost;
245         $record->userid = $user2->id;
246         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
247         $filename = 'shouldbeanimage.jpg';
248         // Add a fake inline image to the post.
249         $filerecordinline = array(
250             'contextid' => $forum1context->id,
251             'component' => 'mod_forum',
252             'filearea'  => 'post',
253             'itemid'    => $discussion1reply1->id,
254             'filepath'  => '/',
255             'filename'  => $filename,
256         );
257         $fs = get_file_storage();
258         $timepost = time();
259         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
261         $record->parent = $discussion1reply1->id;
262         $record->userid = $user3->id;
263         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
265         // Enrol the user in the  course.
266         $enrol = enrol_get_plugin('manual');
267         // Following line enrol and assign default role id to the user.
268         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
269         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
270         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
272         // Delete one user, to test that we still receive posts by this user.
273         delete_user($user3);
275         // Create what we expect to be returned when querying the discussion.
276         $expectedposts = array(
277             'posts' => array(),
278             'ratinginfo' => array(
279                 'contextid' => $forum1context->id,
280                 'component' => 'mod_forum',
281                 'ratingarea' => 'post',
282                 'canviewall' => null,
283                 'canviewany' => null,
284                 'scales' => array(),
285                 'ratings' => array(),
286             ),
287             'warnings' => array(),
288         );
290         // User pictures are initially empty, we should get the links once the external function is called.
291         $expectedposts['posts'][] = array(
292             'id' => $discussion1reply2->id,
293             'discussion' => $discussion1reply2->discussion,
294             'parent' => $discussion1reply2->parent,
295             'userid' => (int) $discussion1reply2->userid,
296             'created' => $discussion1reply2->created,
297             'modified' => $discussion1reply2->modified,
298             'mailed' => $discussion1reply2->mailed,
299             'subject' => $discussion1reply2->subject,
300             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
301                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
302             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
303             'messagetrust' => $discussion1reply2->messagetrust,
304             'attachment' => $discussion1reply2->attachment,
305             'totalscore' => $discussion1reply2->totalscore,
306             'mailnow' => $discussion1reply2->mailnow,
307             'children' => array(),
308             'canreply' => true,
309             'postread' => false,
310             'userfullname' => fullname($user3),
311             'userpictureurl' => '',
312             'deleted' => false,
313             'isprivatereply' => false,
314         );
316         $expectedposts['posts'][] = array(
317             'id' => $discussion1reply1->id,
318             'discussion' => $discussion1reply1->discussion,
319             'parent' => $discussion1reply1->parent,
320             'userid' => (int) $discussion1reply1->userid,
321             'created' => $discussion1reply1->created,
322             'modified' => $discussion1reply1->modified,
323             'mailed' => $discussion1reply1->mailed,
324             'subject' => $discussion1reply1->subject,
325             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
326                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
327             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
328             'messagetrust' => $discussion1reply1->messagetrust,
329             'attachment' => $discussion1reply1->attachment,
330             'messageinlinefiles' => array(
331                 array(
332                     'filename' => $filename,
333                     'filepath' => '/',
334                     'filesize' => '27',
335                     'fileurl' => moodle_url::make_webservice_pluginfile_url($forum1context->id, 'mod_forum', 'post',
336                                     $discussion1reply1->id, '/', $filename),
337                     'timemodified' => $timepost,
338                     'mimetype' => 'image/jpeg',
339                     'isexternalfile' => false,
340                 )
341             ),
342             'totalscore' => $discussion1reply1->totalscore,
343             'mailnow' => $discussion1reply1->mailnow,
344             'children' => array($discussion1reply2->id),
345             'canreply' => true,
346             'postread' => false,
347             'userfullname' => fullname($user2),
348             'userpictureurl' => '',
349             'deleted' => false,
350             'isprivatereply' => false,
351         );
353         // Test a discussion with two additional posts (total 3 posts).
354         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
355         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
356         $this->assertEquals(3, count($posts['posts']));
358         // Generate here the pictures because we need to wait to the external function to init the theme.
359         $userpicture = new user_picture($user3);
360         $userpicture->size = 1; // Size f1.
361         $expectedposts['posts'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
363         $userpicture = new user_picture($user2);
364         $userpicture->size = 1; // Size f1.
365         $expectedposts['posts'][1]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
367         // Unset the initial discussion post.
368         array_pop($posts['posts']);
369         $this->assertEquals($expectedposts, $posts);
371         // Check we receive the unread count correctly on tracked forum.
372         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
373         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
374         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
375         foreach ($result as $f) {
376             if ($f['id'] == $forum2->id) {
377                 $this->assertEquals(1, $f['unreadpostscount']);
378             }
379         }
381         // Test discussion without additional posts. There should be only one post (the one created by the discussion).
382         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id, 'modified', 'DESC');
383         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
384         $this->assertEquals(1, count($posts['posts']));
386         // Test discussion tracking on not tracked forum.
387         $result = mod_forum_external::view_forum_discussion($discussion1->id);
388         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
389         $this->assertTrue($result['status']);
390         $this->assertEmpty($result['warnings']);
392         // Test posts have not been marked as read.
393         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
394         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
395         foreach ($posts['posts'] as $post) {
396             $this->assertFalse($post['postread']);
397         }
399         // Test discussion tracking on tracked forum.
400         $result = mod_forum_external::view_forum_discussion($discussion3->id);
401         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
402         $this->assertTrue($result['status']);
403         $this->assertEmpty($result['warnings']);
405         // Test posts have been marked as read.
406         $posts = mod_forum_external::get_forum_discussion_posts($discussion3->id, 'modified', 'DESC');
407         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
408         foreach ($posts['posts'] as $post) {
409             $this->assertTrue($post['postread']);
410         }
412         // Check we receive 0 unread posts.
413         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
414         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
415         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
416         foreach ($result as $f) {
417             if ($f['id'] == $forum2->id) {
418                 $this->assertEquals(0, $f['unreadpostscount']);
419             }
420         }
421     }
423     /**
424      * Test get forum posts
425      *
426      * Tests is similar to the get_forum_discussion_posts only utilizing the new return structure and entities
427      */
428     public function test_mod_forum_get_discussion_posts() {
429         global $CFG, $PAGE;
431         $this->resetAfterTest(true);
433         // Set the CFG variable to allow track forums.
434         $CFG->forum_trackreadposts = true;
436         $urlfactory = mod_forum\local\container::get_url_factory();
437         $legacyfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
438         $entityfactory = mod_forum\local\container::get_entity_factory();
440         // Create a user who can track forums.
441         $record = new stdClass();
442         $record->trackforums = true;
443         $user1 = self::getDataGenerator()->create_user($record);
444         // Create a bunch of other users to post.
445         $user2 = self::getDataGenerator()->create_user();
446         $user2entity = $entityfactory->get_author_from_stdclass($user2);
447         $exporteduser2 = [
448             'id' => (int) $user2->id,
449             'fullname' => fullname($user2),
450             'groups' => [],
451             'urls' => [
452                 'profile' => $urlfactory->get_author_profile_url($user2entity),
453                 'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
454             ]
455         ];
456         $user2->fullname = $exporteduser2['fullname'];
458         $user3 = self::getDataGenerator()->create_user(['fullname' => "Mr Pants 1"]);
459         $user3entity = $entityfactory->get_author_from_stdclass($user3);
460         $exporteduser3 = [
461             'id' => (int) $user3->id,
462             'fullname' => fullname($user3),
463             'groups' => [],
464             'urls' => [
465                 'profile' => $urlfactory->get_author_profile_url($user3entity),
466                 'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
467             ]
468         ];
469         $user3->fullname = $exporteduser3['fullname'];
470         $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
472         // Set the first created user to the test user.
473         self::setUser($user1);
475         // Create course to add the module.
476         $course1 = self::getDataGenerator()->create_course();
478         // Forum with tracking off.
479         $record = new stdClass();
480         $record->course = $course1->id;
481         $record->trackingtype = FORUM_TRACKING_OFF;
482         $forum1 = self::getDataGenerator()->create_module('forum', $record);
483         $forum1context = context_module::instance($forum1->cmid);
485         // Forum with tracking enabled.
486         $record = new stdClass();
487         $record->course = $course1->id;
488         $forum2 = self::getDataGenerator()->create_module('forum', $record);
489         $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
490         $forum2context = context_module::instance($forum2->cmid);
492         // Add discussions to the forums.
493         $record = new stdClass();
494         $record->course = $course1->id;
495         $record->userid = $user1->id;
496         $record->forum = $forum1->id;
497         $discussion1 = $forumgenerator->create_discussion($record);
499         $record = new stdClass();
500         $record->course = $course1->id;
501         $record->userid = $user2->id;
502         $record->forum = $forum1->id;
503         $discussion2 = $forumgenerator->create_discussion($record);
505         $record = new stdClass();
506         $record->course = $course1->id;
507         $record->userid = $user2->id;
508         $record->forum = $forum2->id;
509         $discussion3 = $forumgenerator->create_discussion($record);
511         // Add 2 replies to the discussion 1 from different users.
512         $record = new stdClass();
513         $record->discussion = $discussion1->id;
514         $record->parent = $discussion1->firstpost;
515         $record->userid = $user2->id;
516         $discussion1reply1 = $forumgenerator->create_post($record);
517         $filename = 'shouldbeanimage.jpg';
518         // Add a fake inline image to the post.
519         $filerecordinline = array(
520             'contextid' => $forum1context->id,
521             'component' => 'mod_forum',
522             'filearea'  => 'post',
523             'itemid'    => $discussion1reply1->id,
524             'filepath'  => '/',
525             'filename'  => $filename,
526         );
527         $fs = get_file_storage();
528         $timepost = time();
529         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
531         $record->parent = $discussion1reply1->id;
532         $record->userid = $user3->id;
533         $discussion1reply2 = $forumgenerator->create_post($record);
535         // Enrol the user in the  course.
536         $enrol = enrol_get_plugin('manual');
537         // Following line enrol and assign default role id to the user.
538         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
539         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
540         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
542         // Delete one user, to test that we still receive posts by this user.
543         delete_user($user3);
545         // Create what we expect to be returned when querying the discussion.
546         $expectedposts = array(
547             'posts' => array(),
548             'ratinginfo' => array(
549                 'contextid' => $forum1context->id,
550                 'component' => 'mod_forum',
551                 'ratingarea' => 'post',
552                 'canviewall' => null,
553                 'canviewany' => null,
554                 'scales' => array(),
555                 'ratings' => array(),
556             ),
557             'warnings' => array(),
558         );
560         // User pictures are initially empty, we should get the links once the external function is called.
561         $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion);
562         $isolatedurl->params(['parent' => $discussion1reply2->id]);
563         $expectedposts['posts'][] = array(
564             'id' => $discussion1reply2->id,
565             'discussionid' => $discussion1reply2->discussion,
566             'parentid' => $discussion1reply2->parent,
567             'hasparent' => true,
568             'timecreated' => $discussion1reply2->created,
569             'subject' => $discussion1reply2->subject,
570             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
571                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
572             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
573             'unread' => null,
574             'isdeleted' => false,
575             'isprivatereply' => false,
576             'haswordcount' => false,
577             'wordcount' => null,
578             'author'=> $exporteduser3,
579             'attachments' => [],
580             'tags' => [],
581             'html' => [
582                 'rating' => null,
583                 'taglist' => null,
584                 'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser3, $discussion1reply2->created)
585             ],
586             'capabilities' => [
587                 'view' => 1,
588                 'edit' => 0,
589                 'delete' => 0,
590                 'split' => 0,
591                 'reply' => 1,
592                 'export' => 0,
593                 'controlreadstatus' => 0
594             ],
595             'urls' => [
596                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
597                 'viewisolated' => $isolatedurl->out(false),
598                 'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->parent),
599                 'edit' => null,
600                 'delete' =>null,
601                 'split' => null,
602                 'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
603                     'reply' => $discussion1reply2->id
604                 ]))->out(false),
605                 'export' => null,
606                 'markasread' => null,
607                 'markasunread' => null,
608                 'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion),
609             ],
610         );
613         $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
614         $isolatedurl->params(['parent' => $discussion1reply1->id]);
615         $expectedposts['posts'][] = array(
616             'id' => $discussion1reply1->id,
617             'discussionid' => $discussion1reply1->discussion,
618             'parentid' => $discussion1reply1->parent,
619             'hasparent' => true,
620             'timecreated' => $discussion1reply1->created,
621             'subject' => $discussion1reply1->subject,
622             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
623                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
624             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
625             'unread' => null,
626             'isdeleted' => false,
627             'isprivatereply' => false,
628             'haswordcount' => false,
629             'wordcount' => null,
630             'author'=> $exporteduser2,
631             'attachments' => [],
632             'tags' => [],
633             'html' => [
634                 'rating' => null,
635                 'taglist' => null,
636                 'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser2, $discussion1reply1->created)
637             ],
638             'capabilities' => [
639                 'view' => 1,
640                 'edit' => 0,
641                 'delete' => 0,
642                 'split' => 0,
643                 'reply' => 1,
644                 'export' => 0,
645                 'controlreadstatus' => 0
646             ],
647             'urls' => [
648                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
649                 'viewisolated' => $isolatedurl->out(false),
650                 'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->parent),
651                 'edit' => null,
652                 'delete' =>null,
653                 'split' => null,
654                 'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
655                     'reply' => $discussion1reply1->id
656                 ]))->out(false),
657                 'export' => null,
658                 'markasread' => null,
659                 'markasunread' => null,
660                 'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion),
661             ],
662         );
664         // Test a discussion with two additional posts (total 3 posts).
665         $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
666         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
667         $this->assertEquals(3, count($posts['posts']));
669         // Unset the initial discussion post.
670         array_pop($posts['posts']);
671         $this->assertEquals($expectedposts, $posts);
673         // Check we receive the unread count correctly on tracked forum.
674         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
675         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
676         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
677         foreach ($result as $f) {
678             if ($f['id'] == $forum2->id) {
679                 $this->assertEquals(1, $f['unreadpostscount']);
680             }
681         }
683         // Test discussion without additional posts. There should be only one post (the one created by the discussion).
684         $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC');
685         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
686         $this->assertEquals(1, count($posts['posts']));
688         // Test discussion tracking on not tracked forum.
689         $result = mod_forum_external::view_forum_discussion($discussion1->id);
690         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
691         $this->assertTrue($result['status']);
692         $this->assertEmpty($result['warnings']);
694         // Test posts have not been marked as read.
695         $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
696         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
697         foreach ($posts['posts'] as $post) {
698             $this->assertNull($post['unread']);
699         }
701         // Test discussion tracking on tracked forum.
702         $result = mod_forum_external::view_forum_discussion($discussion3->id);
703         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
704         $this->assertTrue($result['status']);
705         $this->assertEmpty($result['warnings']);
707         // Test posts have been marked as read.
708         $posts = mod_forum_external::get_discussion_posts($discussion3->id, 'modified', 'DESC');
709         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
710         foreach ($posts['posts'] as $post) {
711             $this->assertFalse($post['unread']);
712         }
714         // Check we receive 0 unread posts.
715         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
716         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
717         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
718         foreach ($result as $f) {
719             if ($f['id'] == $forum2->id) {
720                 $this->assertEquals(0, $f['unreadpostscount']);
721             }
722         }
723     }
725     /**
726      * Test get forum posts
727      */
728     public function test_mod_forum_get_forum_discussion_posts_deleted() {
729         global $CFG, $PAGE;
731         $this->resetAfterTest(true);
732         $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
734         // Create a course and enrol some users in it.
735         $course1 = self::getDataGenerator()->create_course();
737         // Create users.
738         $user1 = self::getDataGenerator()->create_user();
739         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
740         $user2 = self::getDataGenerator()->create_user();
741         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
743         // Set the first created user to the test user.
744         self::setUser($user1);
746         // Create test data.
747         $forum1 = self::getDataGenerator()->create_module('forum', (object) [
748                 'course' => $course1->id,
749             ]);
750         $forum1context = context_module::instance($forum1->cmid);
752         // Add discussions to the forum.
753         $discussion = $generator->create_discussion((object) [
754                 'course' => $course1->id,
755                 'userid' => $user1->id,
756                 'forum' => $forum1->id,
757             ]);
759         $discussion2 = $generator->create_discussion((object) [
760                 'course' => $course1->id,
761                 'userid' => $user2->id,
762                 'forum' => $forum1->id,
763             ]);
765         // Add replies to the discussion.
766         $discussionreply1 = $generator->create_post((object) [
767                 'discussion' => $discussion->id,
768                 'parent' => $discussion->firstpost,
769                 'userid' => $user2->id,
770             ]);
771         $discussionreply2 = $generator->create_post((object) [
772                 'discussion' => $discussion->id,
773                 'parent' => $discussionreply1->id,
774                 'userid' => $user2->id,
775                 'subject' => '',
776                 'message' => '',
777                 'messageformat' => FORMAT_PLAIN,
778                 'deleted' => 1,
779             ]);
780         $discussionreply3 = $generator->create_post((object) [
781                 'discussion' => $discussion->id,
782                 'parent' => $discussion->firstpost,
783                 'userid' => $user2->id,
784             ]);
786         // Test where some posts have been marked as deleted.
787         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id, 'modified', 'DESC');
788         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
789         $deletedsubject = get_string('privacy:request:delete:post:subject', 'mod_forum');
790         $deletedmessage = get_string('privacy:request:delete:post:message', 'mod_forum');
792         foreach ($posts['posts'] as $post) {
793             if ($post['id'] == $discussionreply2->id) {
794                 $this->assertTrue($post['deleted']);
795                 $this->assertEquals($deletedsubject, $post['subject']);
796                 $this->assertEquals($deletedmessage, $post['message']);
797             } else {
798                 $this->assertFalse($post['deleted']);
799                 $this->assertNotEquals($deletedsubject, $post['subject']);
800                 $this->assertNotEquals($deletedmessage, $post['message']);
801             }
802         }
803     }
805     /**
806      * Test get forum posts (qanda forum)
807      */
808     public function test_mod_forum_get_forum_discussion_posts_qanda() {
809         global $CFG, $DB;
811         $this->resetAfterTest(true);
813         $record = new stdClass();
814         $user1 = self::getDataGenerator()->create_user($record);
815         $user2 = self::getDataGenerator()->create_user();
817         // Set the first created user to the test user.
818         self::setUser($user1);
820         // Create course to add the module.
821         $course1 = self::getDataGenerator()->create_course();
822         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
823         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
825         // Forum with tracking off.
826         $record = new stdClass();
827         $record->course = $course1->id;
828         $record->type = 'qanda';
829         $forum1 = self::getDataGenerator()->create_module('forum', $record);
830         $forum1context = context_module::instance($forum1->cmid);
832         // Add discussions to the forums.
833         $record = new stdClass();
834         $record->course = $course1->id;
835         $record->userid = $user2->id;
836         $record->forum = $forum1->id;
837         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
839         // Add 1 reply (not the actual user).
840         $record = new stdClass();
841         $record->discussion = $discussion1->id;
842         $record->parent = $discussion1->firstpost;
843         $record->userid = $user2->id;
844         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
846         // We still see only the original post.
847         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
848         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
849         $this->assertEquals(1, count($posts['posts']));
851         // Add a new reply, the user is going to be able to see only the original post and their new post.
852         $record = new stdClass();
853         $record->discussion = $discussion1->id;
854         $record->parent = $discussion1->firstpost;
855         $record->userid = $user1->id;
856         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
858         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
859         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
860         $this->assertEquals(2, count($posts['posts']));
862         // Now, we can fake the time of the user post, so he can se the rest of the discussion posts.
863         $discussion1reply2->created -= $CFG->maxeditingtime * 2;
864         $DB->update_record('forum_posts', $discussion1reply2);
866         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
867         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
868         $this->assertEquals(3, count($posts['posts']));
869     }
871     /**
872      * Test get forum discussions paginated
873      */
874     public function test_mod_forum_get_forum_discussions_paginated() {
875         global $USER, $CFG, $DB, $PAGE;
877         $this->resetAfterTest(true);
879         // Set the CFG variable to allow track forums.
880         $CFG->forum_trackreadposts = true;
882         // Create a user who can track forums.
883         $record = new stdClass();
884         $record->trackforums = true;
885         $user1 = self::getDataGenerator()->create_user($record);
886         // Create a bunch of other users to post.
887         $user2 = self::getDataGenerator()->create_user();
888         $user3 = self::getDataGenerator()->create_user();
889         $user4 = self::getDataGenerator()->create_user();
891         // Set the first created user to the test user.
892         self::setUser($user1);
894         // Create courses to add the modules.
895         $course1 = self::getDataGenerator()->create_course();
897         // First forum with tracking off.
898         $record = new stdClass();
899         $record->course = $course1->id;
900         $record->trackingtype = FORUM_TRACKING_OFF;
901         $forum1 = self::getDataGenerator()->create_module('forum', $record);
903         // Add discussions to the forums.
904         $record = new stdClass();
905         $record->course = $course1->id;
906         $record->userid = $user1->id;
907         $record->forum = $forum1->id;
908         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
910         // Add three replies to the discussion 1 from different users.
911         $record = new stdClass();
912         $record->discussion = $discussion1->id;
913         $record->parent = $discussion1->firstpost;
914         $record->userid = $user2->id;
915         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
917         $record->parent = $discussion1reply1->id;
918         $record->userid = $user3->id;
919         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
921         $record->userid = $user4->id;
922         $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
924         // Enrol the user in the first course.
925         $enrol = enrol_get_plugin('manual');
927         // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
928         $enrolinstances = enrol_get_instances($course1->id, true);
929         foreach ($enrolinstances as $courseenrolinstance) {
930             if ($courseenrolinstance->enrol == "manual") {
931                 $instance1 = $courseenrolinstance;
932                 break;
933             }
934         }
935         $enrol->enrol_user($instance1, $user1->id);
937         // Delete one user.
938         delete_user($user4);
940         // Assign capabilities to view discussions for forum 1.
941         $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
942         $context = context_module::instance($cm->id);
943         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
944         $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
946         // Create what we expect to be returned when querying the forums.
948         $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
950         // User pictures are initially empty, we should get the links once the external function is called.
951         $expecteddiscussions = array(
952                 'id' => $discussion1->firstpost,
953                 'name' => $discussion1->name,
954                 'groupid' => $discussion1->groupid,
955                 'timemodified' => $discussion1reply3->created,
956                 'usermodified' => $discussion1reply3->userid,
957                 'timestart' => $discussion1->timestart,
958                 'timeend' => $discussion1->timeend,
959                 'discussion' => $discussion1->id,
960                 'parent' => 0,
961                 'userid' => $discussion1->userid,
962                 'created' => $post1->created,
963                 'modified' => $post1->modified,
964                 'mailed' => $post1->mailed,
965                 'subject' => $post1->subject,
966                 'message' => $post1->message,
967                 'messageformat' => $post1->messageformat,
968                 'messagetrust' => $post1->messagetrust,
969                 'attachment' => $post1->attachment,
970                 'totalscore' => $post1->totalscore,
971                 'mailnow' => $post1->mailnow,
972                 'userfullname' => fullname($user1),
973                 'usermodifiedfullname' => fullname($user4),
974                 'userpictureurl' => '',
975                 'usermodifiedpictureurl' => '',
976                 'numreplies' => 3,
977                 'numunread' => 0,
978                 'pinned' => FORUM_DISCUSSION_UNPINNED,
979                 'locked' => false,
980                 'canreply' => false,
981             );
983         // Call the external function passing forum id.
984         $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
985         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
986         $expectedreturn = array(
987             'discussions' => array($expecteddiscussions),
988             'warnings' => array()
989         );
991         // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
992         $userpicture = new user_picture($user1);
993         $userpicture->size = 1; // Size f1.
994         $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
996         $userpicture = new user_picture($user4);
997         $userpicture->size = 1; // Size f1.
998         $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1000         $this->assertEquals($expectedreturn, $discussions);
1002         // Call without required view discussion capability.
1003         $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1004         try {
1005             mod_forum_external::get_forum_discussions_paginated($forum1->id);
1006             $this->fail('Exception expected due to missing capability.');
1007         } catch (moodle_exception $e) {
1008             $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1009         }
1011         // Unenrol user from second course.
1012         $enrol->unenrol_user($instance1, $user1->id);
1014         // Call for the second course we unenrolled the user from, make sure exception thrown.
1015         try {
1016             mod_forum_external::get_forum_discussions_paginated($forum1->id);
1017             $this->fail('Exception expected due to being unenrolled from the course.');
1018         } catch (moodle_exception $e) {
1019             $this->assertEquals('requireloginerror', $e->errorcode);
1020         }
1021     }
1023     /**
1024      * Test get forum discussions paginated (qanda forums)
1025      */
1026     public function test_mod_forum_get_forum_discussions_paginated_qanda() {
1028         $this->resetAfterTest(true);
1030         // Create courses to add the modules.
1031         $course = self::getDataGenerator()->create_course();
1033         $user1 = self::getDataGenerator()->create_user();
1034         $user2 = self::getDataGenerator()->create_user();
1036         // First forum with tracking off.
1037         $record = new stdClass();
1038         $record->course = $course->id;
1039         $record->type = 'qanda';
1040         $forum = self::getDataGenerator()->create_module('forum', $record);
1042         // Add discussions to the forums.
1043         $discussionrecord = new stdClass();
1044         $discussionrecord->course = $course->id;
1045         $discussionrecord->userid = $user2->id;
1046         $discussionrecord->forum = $forum->id;
1047         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
1049         self::setAdminUser();
1050         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1051         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1053         $this->assertCount(1, $discussions['discussions']);
1054         $this->assertCount(0, $discussions['warnings']);
1056         self::setUser($user1);
1057         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1059         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1060         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1062         $this->assertCount(1, $discussions['discussions']);
1063         $this->assertCount(0, $discussions['warnings']);
1065     }
1067     /**
1068      * Test add_discussion_post
1069      */
1070     public function test_add_discussion_post() {
1071         global $CFG;
1073         $this->resetAfterTest(true);
1075         $user = self::getDataGenerator()->create_user();
1076         $otheruser = self::getDataGenerator()->create_user();
1078         self::setAdminUser();
1080         // Create course to add the module.
1081         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1083         // Forum with tracking off.
1084         $record = new stdClass();
1085         $record->course = $course->id;
1086         $forum = self::getDataGenerator()->create_module('forum', $record);
1087         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1088         $forumcontext = context_module::instance($forum->cmid);
1090         // Add discussions to the forums.
1091         $record = new stdClass();
1092         $record->course = $course->id;
1093         $record->userid = $user->id;
1094         $record->forum = $forum->id;
1095         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1097         // Try to post (user not enrolled).
1098         self::setUser($user);
1099         try {
1100             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1101             $this->fail('Exception expected due to being unenrolled from the course.');
1102         } catch (moodle_exception $e) {
1103             $this->assertEquals('requireloginerror', $e->errorcode);
1104         }
1106         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1107         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
1109         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1110         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1112         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1113         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1114         // We receive the discussion and the post.
1115         $this->assertEquals(2, count($posts['posts']));
1117         $tested = false;
1118         foreach ($posts['posts'] as $thispost) {
1119             if ($createdpost['postid'] == $thispost['id']) {
1120                 $this->assertEquals('some subject', $thispost['subject']);
1121                 $this->assertEquals('some text here...', $thispost['message']);
1122                 $tested = true;
1123             }
1124         }
1125         $this->assertTrue($tested);
1127         // Test inline and regular attachment in post
1128         // Create a file in a draft area for inline attachments.
1129         $draftidinlineattach = file_get_unused_draft_itemid();
1130         $draftidattach = file_get_unused_draft_itemid();
1131         self::setUser($user);
1132         $usercontext = context_user::instance($user->id);
1133         $filepath = '/';
1134         $filearea = 'draft';
1135         $component = 'user';
1136         $filenameimg = 'shouldbeanimage.txt';
1137         $filerecordinline = array(
1138             'contextid' => $usercontext->id,
1139             'component' => $component,
1140             'filearea'  => $filearea,
1141             'itemid'    => $draftidinlineattach,
1142             'filepath'  => $filepath,
1143             'filename'  => $filenameimg,
1144         );
1145         $fs = get_file_storage();
1147         // Create a file in a draft area for regular attachments.
1148         $filerecordattach = $filerecordinline;
1149         $attachfilename = 'attachment.txt';
1150         $filerecordattach['filename'] = $attachfilename;
1151         $filerecordattach['itemid'] = $draftidattach;
1152         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
1153         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1155         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1156                          array('name' => 'attachmentsid', 'value' => $draftidattach));
1157         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot
1158                      . "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}"
1159                      . '" alt="inlineimage">.';
1160         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment',
1161                                                                $dummytext, $options);
1162         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1164         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1165         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1166         // We receive the discussion and the post.
1167         // Can't guarantee order of posts during tests.
1168         $postfound = false;
1169         foreach ($posts['posts'] as $thispost) {
1170             if ($createdpost['postid'] == $thispost['id']) {
1171                 $this->assertEquals($createdpost['postid'], $thispost['id']);
1172                 $this->assertEquals($thispost['attachment'], 1, "There should be a non-inline attachment");
1173                 $this->assertCount(1, $thispost['attachments'], "There should be 1 attachment");
1174                 $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1175                 $this->assertContains('pluginfile.php', $thispost['message']);
1176                 $postfound = true;
1177                 break;
1178             }
1179         }
1181         $this->assertTrue($postfound);
1183         // Check not posting in groups the user is not member of.
1184         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1185         groups_add_member($group->id, $otheruser->id);
1187         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1188         $record->forum = $forum->id;
1189         $record->userid = $otheruser->id;
1190         $record->groupid = $group->id;
1191         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1193         try {
1194             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1195             $this->fail('Exception expected due to invalid permissions for posting.');
1196         } catch (moodle_exception $e) {
1197             $this->assertEquals('nopostforum', $e->errorcode);
1198         }
1200     }
1202     /*
1203      * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
1204      */
1205     public function test_add_discussion() {
1206         global $CFG, $USER;
1207         $this->resetAfterTest(true);
1209         // Create courses to add the modules.
1210         $course = self::getDataGenerator()->create_course();
1212         $user1 = self::getDataGenerator()->create_user();
1213         $user2 = self::getDataGenerator()->create_user();
1215         // First forum with tracking off.
1216         $record = new stdClass();
1217         $record->course = $course->id;
1218         $record->type = 'news';
1219         $forum = self::getDataGenerator()->create_module('forum', $record);
1221         self::setUser($user1);
1222         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1224         try {
1225             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1226             $this->fail('Exception expected due to invalid permissions.');
1227         } catch (moodle_exception $e) {
1228             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1229         }
1231         self::setAdminUser();
1232         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1233         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1235         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1236         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1238         $this->assertCount(1, $discussions['discussions']);
1239         $this->assertCount(0, $discussions['warnings']);
1241         $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']);
1242         $this->assertEquals(-1, $discussions['discussions'][0]['groupid']);
1243         $this->assertEquals('the subject', $discussions['discussions'][0]['subject']);
1244         $this->assertEquals('some text here...', $discussions['discussions'][0]['message']);
1246         $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1,
1247                                                                 array('options' => array('name' => 'discussionpinned',
1248                                                                                          'value' => true)));
1249         $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...');
1250         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1251         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1252         $this->assertCount(3, $discussions['discussions']);
1253         $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']);
1255         // Test inline and regular attachment in new discussion
1256         // Create a file in a draft area for inline attachments.
1258         $fs = get_file_storage();
1260         $draftidinlineattach = file_get_unused_draft_itemid();
1261         $draftidattach = file_get_unused_draft_itemid();
1263         $usercontext = context_user::instance($USER->id);
1264         $filepath = '/';
1265         $filearea = 'draft';
1266         $component = 'user';
1267         $filenameimg = 'shouldbeanimage.txt';
1268         $filerecord = array(
1269             'contextid' => $usercontext->id,
1270             'component' => $component,
1271             'filearea'  => $filearea,
1272             'itemid'    => $draftidinlineattach,
1273             'filepath'  => $filepath,
1274             'filename'  => $filenameimg,
1275         );
1277         // Create a file in a draft area for regular attachments.
1278         $filerecordattach = $filerecord;
1279         $attachfilename = 'attachment.txt';
1280         $filerecordattach['filename'] = $attachfilename;
1281         $filerecordattach['itemid'] = $draftidattach;
1282         $fs->create_file_from_string($filerecord, 'image contents (not really)');
1283         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1285         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
1286                     "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
1287                     '" alt="inlineimage">.';
1289         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1290                          array('name' => 'attachmentsid', 'value' => $draftidattach));
1291         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject',
1292                                                                 $dummytext, -1, $options);
1293         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1295         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1296         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1298         $this->assertCount(4, $discussions['discussions']);
1299         $this->assertCount(0, $createddiscussion['warnings']);
1300         // Can't guarantee order of posts during tests.
1301         $postfound = false;
1302         foreach ($discussions['discussions'] as $thisdiscussion) {
1303             if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) {
1304                 $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment");
1305                 $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment");
1306                 $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1307                 $this->assertNotContains('draftfile.php', $thisdiscussion['message']);
1308                 $this->assertContains('pluginfile.php', $thisdiscussion['message']);
1309                 $postfound = true;
1310                 break;
1311             }
1312         }
1314         $this->assertTrue($postfound);
1315     }
1317     /**
1318      * Test adding discussions in a course with gorups
1319      */
1320     public function test_add_discussion_in_course_with_groups() {
1321         global $CFG;
1323         $this->resetAfterTest(true);
1325         // Create course to add the module.
1326         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1327         $user = self::getDataGenerator()->create_user();
1328         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1330         // Forum forcing separate gropus.
1331         $record = new stdClass();
1332         $record->course = $course->id;
1333         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1335         // Try to post (user not enrolled).
1336         self::setUser($user);
1338         // The user is not enroled in any group, try to post in a forum with separate groups.
1339         try {
1340             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1341             $this->fail('Exception expected due to invalid group permissions.');
1342         } catch (moodle_exception $e) {
1343             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1344         }
1346         try {
1347             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0);
1348             $this->fail('Exception expected due to invalid group permissions.');
1349         } catch (moodle_exception $e) {
1350             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1351         }
1353         // Create a group.
1354         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1356         // Try to post in a group the user is not enrolled.
1357         try {
1358             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1359             $this->fail('Exception expected due to invalid group permissions.');
1360         } catch (moodle_exception $e) {
1361             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1362         }
1364         // Add the user to a group.
1365         groups_add_member($group->id, $user->id);
1367         // Try to post in a group the user is not enrolled.
1368         try {
1369             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1);
1370             $this->fail('Exception expected due to invalid group.');
1371         } catch (moodle_exception $e) {
1372             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1373         }
1375         // Nost add the discussion using a valid group.
1376         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1377         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1379         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1380         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1382         $this->assertCount(1, $discussions['discussions']);
1383         $this->assertCount(0, $discussions['warnings']);
1384         $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']);
1385         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1387         // Now add a discussions without indicating a group. The function should guess the correct group.
1388         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1389         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1391         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1392         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1394         $this->assertCount(2, $discussions['discussions']);
1395         $this->assertCount(0, $discussions['warnings']);
1396         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1397         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1399         // Enrol the same user in other group.
1400         $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1401         groups_add_member($group2->id, $user->id);
1403         // Now add a discussions without indicating a group. The function should guess the correct group (the first one).
1404         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1405         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1407         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1408         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1410         $this->assertCount(3, $discussions['discussions']);
1411         $this->assertCount(0, $discussions['warnings']);
1412         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1413         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1414         $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']);
1416     }
1418     /*
1419      * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests.
1420      */
1421     public function test_can_add_discussion() {
1422         global $DB;
1423         $this->resetAfterTest(true);
1425         // Create courses to add the modules.
1426         $course = self::getDataGenerator()->create_course();
1428         $user = self::getDataGenerator()->create_user();
1430         // First forum with tracking off.
1431         $record = new stdClass();
1432         $record->course = $course->id;
1433         $record->type = 'news';
1434         $forum = self::getDataGenerator()->create_module('forum', $record);
1436         // User with no permissions to add in a news forum.
1437         self::setUser($user);
1438         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1440         $result = mod_forum_external::can_add_discussion($forum->id);
1441         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1442         $this->assertFalse($result['status']);
1443         $this->assertFalse($result['canpindiscussions']);
1444         $this->assertTrue($result['cancreateattachment']);
1446         // Disable attachments.
1447         $DB->set_field('forum', 'maxattachments', 0, array('id' => $forum->id));
1448         $result = mod_forum_external::can_add_discussion($forum->id);
1449         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1450         $this->assertFalse($result['status']);
1451         $this->assertFalse($result['canpindiscussions']);
1452         $this->assertFalse($result['cancreateattachment']);
1453         $DB->set_field('forum', 'maxattachments', 1, array('id' => $forum->id));    // Enable attachments again.
1455         self::setAdminUser();
1456         $result = mod_forum_external::can_add_discussion($forum->id);
1457         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1458         $this->assertTrue($result['status']);
1459         $this->assertTrue($result['canpindiscussions']);
1460         $this->assertTrue($result['cancreateattachment']);
1461     }
1463     /*
1464      * A basic test to make sure users cannot post to forum after the cutoff date.
1465      */
1466     public function test_can_add_discussion_after_cutoff() {
1467         $this->resetAfterTest(true);
1469         // Create courses to add the modules.
1470         $course = self::getDataGenerator()->create_course();
1472         $user = self::getDataGenerator()->create_user();
1474         // Create a forum with cutoff date set to a past date.
1475         $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'cutoffdate' => time() - 1]);
1477         // User with no mod/forum:canoverridecutoff capability.
1478         self::setUser($user);
1479         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1481         $result = mod_forum_external::can_add_discussion($forum->id);
1482         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1483         $this->assertFalse($result['status']);
1485         self::setAdminUser();
1486         $result = mod_forum_external::can_add_discussion($forum->id);
1487         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1488         $this->assertTrue($result['status']);
1489     }
1491     /**
1492      * Test get forum posts discussions including rating information.
1493      */
1494     public function test_mod_forum_get_forum_discussion_rating_information() {
1495         global $DB, $CFG;
1496         require_once($CFG->dirroot . '/rating/lib.php');
1498         $this->resetAfterTest(true);
1500         $user1 = self::getDataGenerator()->create_user();
1501         $user2 = self::getDataGenerator()->create_user();
1502         $user3 = self::getDataGenerator()->create_user();
1503         $teacher = self::getDataGenerator()->create_user();
1505         // Create course to add the module.
1506         $course = self::getDataGenerator()->create_course();
1508         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1509         $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1510         $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual');
1511         $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual');
1512         $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual');
1513         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
1515         // Create the forum.
1516         $record = new stdClass();
1517         $record->course = $course->id;
1518         // Set Aggregate type = Average of ratings.
1519         $record->assessed = RATING_AGGREGATE_AVERAGE;
1520         $record->scale = 100;
1521         $forum = self::getDataGenerator()->create_module('forum', $record);
1522         $context = context_module::instance($forum->cmid);
1524         // Add discussion to the forum.
1525         $record = new stdClass();
1526         $record->course = $course->id;
1527         $record->userid = $user1->id;
1528         $record->forum = $forum->id;
1529         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1531         // Retrieve the first post.
1532         $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
1534         // Rate the discussion as user2.
1535         $rating1 = new stdClass();
1536         $rating1->contextid = $context->id;
1537         $rating1->component = 'mod_forum';
1538         $rating1->ratingarea = 'post';
1539         $rating1->itemid = $post->id;
1540         $rating1->rating = 50;
1541         $rating1->scaleid = 100;
1542         $rating1->userid = $user2->id;
1543         $rating1->timecreated = time();
1544         $rating1->timemodified = time();
1545         $rating1->id = $DB->insert_record('rating', $rating1);
1547         // Rate the discussion as user3.
1548         $rating2 = new stdClass();
1549         $rating2->contextid = $context->id;
1550         $rating2->component = 'mod_forum';
1551         $rating2->ratingarea = 'post';
1552         $rating2->itemid = $post->id;
1553         $rating2->rating = 100;
1554         $rating2->scaleid = 100;
1555         $rating2->userid = $user3->id;
1556         $rating2->timecreated = time() + 1;
1557         $rating2->timemodified = time() + 1;
1558         $rating2->id = $DB->insert_record('rating', $rating2);
1560         // Retrieve the rating for the post as student.
1561         $this->setUser($user1);
1562         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1563         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1564         $this->assertCount(1, $posts['ratinginfo']['ratings']);
1565         $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
1566         $this->assertFalse($posts['ratinginfo']['canviewall']);
1567         $this->assertFalse($posts['ratinginfo']['ratings'][0]['canrate']);
1568         $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
1569         $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
1571         // Retrieve the rating for the post as teacher.
1572         $this->setUser($teacher);
1573         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1574         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1575         $this->assertCount(1, $posts['ratinginfo']['ratings']);
1576         $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
1577         $this->assertTrue($posts['ratinginfo']['canviewall']);
1578         $this->assertTrue($posts['ratinginfo']['ratings'][0]['canrate']);
1579         $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
1580         $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
1581     }
1583     /**
1584      * Test mod_forum_get_forum_access_information.
1585      */
1586     public function test_mod_forum_get_forum_access_information() {
1587         global $DB;
1589         $this->resetAfterTest(true);
1591         $student = self::getDataGenerator()->create_user();
1592         $course = self::getDataGenerator()->create_course();
1593         // Create the forum.
1594         $record = new stdClass();
1595         $record->course = $course->id;
1596         $forum = self::getDataGenerator()->create_module('forum', $record);
1598         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1599         $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
1601         self::setUser($student);
1602         $result = mod_forum_external::get_forum_access_information($forum->id);
1603         $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
1605         // Check default values for capabilities.
1606         $enabledcaps = array('canviewdiscussion', 'canstartdiscussion', 'canreplypost', 'canviewrating', 'cancreateattachment',
1607             'canexportownpost', 'candeleteownpost', 'canallowforcesubscribe');
1609         unset($result['warnings']);
1610         foreach ($result as $capname => $capvalue) {
1611             if (in_array($capname, $enabledcaps)) {
1612                 $this->assertTrue($capvalue);
1613             } else {
1614                 $this->assertFalse($capvalue);
1615             }
1616         }
1617         // Now, unassign some capabilities.
1618         unassign_capability('mod/forum:deleteownpost', $studentrole->id);
1619         unassign_capability('mod/forum:allowforcesubscribe', $studentrole->id);
1620         array_pop($enabledcaps);
1621         array_pop($enabledcaps);
1622         accesslib_clear_all_caches_for_unit_testing();
1624         $result = mod_forum_external::get_forum_access_information($forum->id);
1625         $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
1626         unset($result['warnings']);
1627         foreach ($result as $capname => $capvalue) {
1628             if (in_array($capname, $enabledcaps)) {
1629                 $this->assertTrue($capvalue);
1630             } else {
1631                 $this->assertFalse($capvalue);
1632             }
1633         }
1634     }
1636     /**
1637      * Test add_discussion_post
1638      */
1639     public function test_add_discussion_post_private() {
1640         global $DB;
1642         $this->resetAfterTest(true);
1644         self::setAdminUser();
1646         // Create course to add the module.
1647         $course = self::getDataGenerator()->create_course();
1649         // Standard forum.
1650         $record = new stdClass();
1651         $record->course = $course->id;
1652         $forum = self::getDataGenerator()->create_module('forum', $record);
1653         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1654         $forumcontext = context_module::instance($forum->cmid);
1655         $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
1657         // Create an enrol users.
1658         $student1 = self::getDataGenerator()->create_user();
1659         $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
1660         $student2 = self::getDataGenerator()->create_user();
1661         $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
1662         $teacher1 = self::getDataGenerator()->create_user();
1663         $this->getDataGenerator()->enrol_user($teacher1->id, $course->id, 'editingteacher');
1664         $teacher2 = self::getDataGenerator()->create_user();
1665         $this->getDataGenerator()->enrol_user($teacher2->id, $course->id, 'editingteacher');
1667         // Add a new discussion to the forum.
1668         self::setUser($student1);
1669         $record = new stdClass();
1670         $record->course = $course->id;
1671         $record->userid = $student1->id;
1672         $record->forum = $forum->id;
1673         $discussion = $generator->create_discussion($record);
1675         // Have the teacher reply privately.
1676         self::setUser($teacher1);
1677         $post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...', [
1678                 [
1679                     'name' => 'private',
1680                     'value' => true,
1681                 ],
1682             ]);
1683         $post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post);
1684         $privatereply = $DB->get_record('forum_posts', array('id' => $post['postid']));
1685         $this->assertEquals($student1->id, $privatereply->privatereplyto);
1686         // Bump the time of the private reply to ensure order.
1687         $privatereply->created++;
1688         $privatereply->modified = $privatereply->created;
1689         $DB->update_record('forum_posts', $privatereply);
1691         // The teacher will receive their private reply.
1692         self::setUser($teacher1);
1693         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1694         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1695         $this->assertEquals(2, count($posts['posts']));
1696         $this->assertTrue($posts['posts'][0]['isprivatereply']);
1698         // Another teacher on the course will also receive the private reply.
1699         self::setUser($teacher2);
1700         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1701         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1702         $this->assertEquals(2, count($posts['posts']));
1703         $this->assertTrue($posts['posts'][0]['isprivatereply']);
1705         // The student will receive the private reply.
1706         self::setUser($student1);
1707         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1708         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1709         $this->assertEquals(2, count($posts['posts']));
1710         $this->assertTrue($posts['posts'][0]['isprivatereply']);
1712         // Another student will not receive the private reply.
1713         self::setUser($student2);
1714         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1715         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1716         $this->assertEquals(1, count($posts['posts']));
1717         $this->assertFalse($posts['posts'][0]['isprivatereply']);
1719         // A user cannot reply to a private reply.
1720         self::setUser($teacher2);
1721         $this->expectException('coding_exception');
1722         $post = mod_forum_external::add_discussion_post($privatereply->id, 'some subject', 'some text here...', [
1723                 'options' => [
1724                     'name' => 'private',
1725                     'value' => false,
1726                 ],
1727             ]);
1728     }