Merge branch 'MDL-58399-master' of git://github.com/jleyva/moodle
[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             'warnings' => array(),
279         );
281         // User pictures are initially empty, we should get the links once the external function is called.
282         $expectedposts['posts'][] = array(
283             'id' => $discussion1reply2->id,
284             'discussion' => $discussion1reply2->discussion,
285             'parent' => $discussion1reply2->parent,
286             'userid' => (int) $discussion1reply2->userid,
287             'created' => $discussion1reply2->created,
288             'modified' => $discussion1reply2->modified,
289             'mailed' => $discussion1reply2->mailed,
290             'subject' => $discussion1reply2->subject,
291             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
292                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
293             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
294             'messagetrust' => $discussion1reply2->messagetrust,
295             'attachment' => $discussion1reply2->attachment,
296             'totalscore' => $discussion1reply2->totalscore,
297             'mailnow' => $discussion1reply2->mailnow,
298             'children' => array(),
299             'canreply' => true,
300             'postread' => false,
301             'userfullname' => fullname($user3),
302             'userpictureurl' => ''
303         );
305         $expectedposts['posts'][] = array(
306             'id' => $discussion1reply1->id,
307             'discussion' => $discussion1reply1->discussion,
308             'parent' => $discussion1reply1->parent,
309             'userid' => (int) $discussion1reply1->userid,
310             'created' => $discussion1reply1->created,
311             'modified' => $discussion1reply1->modified,
312             'mailed' => $discussion1reply1->mailed,
313             'subject' => $discussion1reply1->subject,
314             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
315                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
316             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
317             'messagetrust' => $discussion1reply1->messagetrust,
318             'attachment' => $discussion1reply1->attachment,
319             'messageinlinefiles' => array(
320                 array(
321                     'filename' => $filename,
322                     'filepath' => '/',
323                     'filesize' => '27',
324                     'fileurl' => moodle_url::make_webservice_pluginfile_url($forum1context->id, 'mod_forum', 'post',
325                                     $discussion1reply1->id, '/', $filename),
326                     'timemodified' => $timepost,
327                     'mimetype' => 'image/jpeg',
328                     'isexternalfile' => false,
329                 )
330             ),
331             'totalscore' => $discussion1reply1->totalscore,
332             'mailnow' => $discussion1reply1->mailnow,
333             'children' => array($discussion1reply2->id),
334             'canreply' => true,
335             'postread' => false,
336             'userfullname' => fullname($user2),
337             'userpictureurl' => ''
338         );
340         // Test a discussion with two additional posts (total 3 posts).
341         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
342         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
343         $this->assertEquals(3, count($posts['posts']));
345         // Generate here the pictures because we need to wait to the external function to init the theme.
346         $userpicture = new user_picture($user3);
347         $userpicture->size = 1; // Size f1.
348         $expectedposts['posts'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
350         $userpicture = new user_picture($user2);
351         $userpicture->size = 1; // Size f1.
352         $expectedposts['posts'][1]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
354         // Unset the initial discussion post.
355         array_pop($posts['posts']);
356         $this->assertEquals($expectedposts, $posts);
358         // Check we receive the unread count correctly on tracked forum.
359         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
360         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
361         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
362         foreach ($result as $f) {
363             if ($f['id'] == $forum2->id) {
364                 $this->assertEquals(1, $f['unreadpostscount']);
365             }
366         }
368         // Test discussion without additional posts. There should be only one post (the one created by the discussion).
369         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id, 'modified', 'DESC');
370         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
371         $this->assertEquals(1, count($posts['posts']));
373         // Test discussion tracking on not tracked forum.
374         $result = mod_forum_external::view_forum_discussion($discussion1->id);
375         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
376         $this->assertTrue($result['status']);
377         $this->assertEmpty($result['warnings']);
379         // Test posts have not been marked as read.
380         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
381         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
382         foreach ($posts['posts'] as $post) {
383             $this->assertFalse($post['postread']);
384         }
386         // Test discussion tracking on tracked forum.
387         $result = mod_forum_external::view_forum_discussion($discussion3->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 been marked as read.
393         $posts = mod_forum_external::get_forum_discussion_posts($discussion3->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->assertTrue($post['postread']);
397         }
399         // Check we receive 0 unread posts.
400         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
401         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
402         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
403         foreach ($result as $f) {
404             if ($f['id'] == $forum2->id) {
405                 $this->assertEquals(0, $f['unreadpostscount']);
406             }
407         }
408     }
410     /**
411      * Test get forum posts (qanda forum)
412      */
413     public function test_mod_forum_get_forum_discussion_posts_qanda() {
414         global $CFG, $DB;
416         $this->resetAfterTest(true);
418         $record = new stdClass();
419         $user1 = self::getDataGenerator()->create_user($record);
420         $user2 = self::getDataGenerator()->create_user();
422         // Set the first created user to the test user.
423         self::setUser($user1);
425         // Create course to add the module.
426         $course1 = self::getDataGenerator()->create_course();
427         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
428         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
430         // Forum with tracking off.
431         $record = new stdClass();
432         $record->course = $course1->id;
433         $record->type = 'qanda';
434         $forum1 = self::getDataGenerator()->create_module('forum', $record);
435         $forum1context = context_module::instance($forum1->cmid);
437         // Add discussions to the forums.
438         $record = new stdClass();
439         $record->course = $course1->id;
440         $record->userid = $user2->id;
441         $record->forum = $forum1->id;
442         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
444         // Add 1 reply (not the actual user).
445         $record = new stdClass();
446         $record->discussion = $discussion1->id;
447         $record->parent = $discussion1->firstpost;
448         $record->userid = $user2->id;
449         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
451         // We still see only the original post.
452         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
453         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
454         $this->assertEquals(1, count($posts['posts']));
456         // Add a new reply, the user is going to be able to see only the original post and their new post.
457         $record = new stdClass();
458         $record->discussion = $discussion1->id;
459         $record->parent = $discussion1->firstpost;
460         $record->userid = $user1->id;
461         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
463         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
464         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
465         $this->assertEquals(2, count($posts['posts']));
467         // Now, we can fake the time of the user post, so he can se the rest of the discussion posts.
468         $discussion1reply2->created -= $CFG->maxeditingtime * 2;
469         $DB->update_record('forum_posts', $discussion1reply2);
471         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
472         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
473         $this->assertEquals(3, count($posts['posts']));
474     }
476     /**
477      * Test get forum discussions paginated
478      */
479     public function test_mod_forum_get_forum_discussions_paginated() {
480         global $USER, $CFG, $DB, $PAGE;
482         $this->resetAfterTest(true);
484         // Set the CFG variable to allow track forums.
485         $CFG->forum_trackreadposts = true;
487         // Create a user who can track forums.
488         $record = new stdClass();
489         $record->trackforums = true;
490         $user1 = self::getDataGenerator()->create_user($record);
491         // Create a bunch of other users to post.
492         $user2 = self::getDataGenerator()->create_user();
493         $user3 = self::getDataGenerator()->create_user();
494         $user4 = self::getDataGenerator()->create_user();
496         // Set the first created user to the test user.
497         self::setUser($user1);
499         // Create courses to add the modules.
500         $course1 = self::getDataGenerator()->create_course();
502         // First forum with tracking off.
503         $record = new stdClass();
504         $record->course = $course1->id;
505         $record->trackingtype = FORUM_TRACKING_OFF;
506         $forum1 = self::getDataGenerator()->create_module('forum', $record);
508         // Add discussions to the forums.
509         $record = new stdClass();
510         $record->course = $course1->id;
511         $record->userid = $user1->id;
512         $record->forum = $forum1->id;
513         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
515         // Add three replies to the discussion 1 from different users.
516         $record = new stdClass();
517         $record->discussion = $discussion1->id;
518         $record->parent = $discussion1->firstpost;
519         $record->userid = $user2->id;
520         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
522         $record->parent = $discussion1reply1->id;
523         $record->userid = $user3->id;
524         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
526         $record->userid = $user4->id;
527         $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
529         // Enrol the user in the first course.
530         $enrol = enrol_get_plugin('manual');
532         // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
533         $enrolinstances = enrol_get_instances($course1->id, true);
534         foreach ($enrolinstances as $courseenrolinstance) {
535             if ($courseenrolinstance->enrol == "manual") {
536                 $instance1 = $courseenrolinstance;
537                 break;
538             }
539         }
540         $enrol->enrol_user($instance1, $user1->id);
542         // Delete one user.
543         delete_user($user4);
545         // Assign capabilities to view discussions for forum 1.
546         $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
547         $context = context_module::instance($cm->id);
548         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
549         $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
551         // Create what we expect to be returned when querying the forums.
553         $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
555         // User pictures are initially empty, we should get the links once the external function is called.
556         $expecteddiscussions = array(
557                 'id' => $discussion1->firstpost,
558                 'name' => $discussion1->name,
559                 'groupid' => $discussion1->groupid,
560                 'timemodified' => $discussion1reply3->created,
561                 'usermodified' => $discussion1reply3->userid,
562                 'timestart' => $discussion1->timestart,
563                 'timeend' => $discussion1->timeend,
564                 'discussion' => $discussion1->id,
565                 'parent' => 0,
566                 'userid' => $discussion1->userid,
567                 'created' => $post1->created,
568                 'modified' => $post1->modified,
569                 'mailed' => $post1->mailed,
570                 'subject' => $post1->subject,
571                 'message' => $post1->message,
572                 'messageformat' => $post1->messageformat,
573                 'messagetrust' => $post1->messagetrust,
574                 'attachment' => $post1->attachment,
575                 'totalscore' => $post1->totalscore,
576                 'mailnow' => $post1->mailnow,
577                 'userfullname' => fullname($user1),
578                 'usermodifiedfullname' => fullname($user4),
579                 'userpictureurl' => '',
580                 'usermodifiedpictureurl' => '',
581                 'numreplies' => 3,
582                 'numunread' => 0,
583                 'pinned' => FORUM_DISCUSSION_UNPINNED,
584                 'locked' => false,
585                 'canreply' => false,
586             );
588         // Call the external function passing forum id.
589         $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
590         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
591         $expectedreturn = array(
592             'discussions' => array($expecteddiscussions),
593             'warnings' => array()
594         );
596         // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
597         $userpicture = new user_picture($user1);
598         $userpicture->size = 1; // Size f1.
599         $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
601         $userpicture = new user_picture($user4);
602         $userpicture->size = 1; // Size f1.
603         $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
605         $this->assertEquals($expectedreturn, $discussions);
607         // Call without required view discussion capability.
608         $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
609         try {
610             mod_forum_external::get_forum_discussions_paginated($forum1->id);
611             $this->fail('Exception expected due to missing capability.');
612         } catch (moodle_exception $e) {
613             $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
614         }
616         // Unenrol user from second course.
617         $enrol->unenrol_user($instance1, $user1->id);
619         // Call for the second course we unenrolled the user from, make sure exception thrown.
620         try {
621             mod_forum_external::get_forum_discussions_paginated($forum1->id);
622             $this->fail('Exception expected due to being unenrolled from the course.');
623         } catch (moodle_exception $e) {
624             $this->assertEquals('requireloginerror', $e->errorcode);
625         }
626     }
628     /**
629      * Test get forum discussions paginated (qanda forums)
630      */
631     public function test_mod_forum_get_forum_discussions_paginated_qanda() {
633         $this->resetAfterTest(true);
635         // Create courses to add the modules.
636         $course = self::getDataGenerator()->create_course();
638         $user1 = self::getDataGenerator()->create_user();
639         $user2 = self::getDataGenerator()->create_user();
641         // First forum with tracking off.
642         $record = new stdClass();
643         $record->course = $course->id;
644         $record->type = 'qanda';
645         $forum = self::getDataGenerator()->create_module('forum', $record);
647         // Add discussions to the forums.
648         $discussionrecord = new stdClass();
649         $discussionrecord->course = $course->id;
650         $discussionrecord->userid = $user2->id;
651         $discussionrecord->forum = $forum->id;
652         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
654         self::setAdminUser();
655         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
656         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
658         $this->assertCount(1, $discussions['discussions']);
659         $this->assertCount(0, $discussions['warnings']);
661         self::setUser($user1);
662         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
664         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
665         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
667         $this->assertCount(1, $discussions['discussions']);
668         $this->assertCount(0, $discussions['warnings']);
670     }
672     /**
673      * Test add_discussion_post
674      */
675     public function test_add_discussion_post() {
676         global $CFG;
678         $this->resetAfterTest(true);
680         $user = self::getDataGenerator()->create_user();
681         $otheruser = self::getDataGenerator()->create_user();
683         self::setAdminUser();
685         // Create course to add the module.
686         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
688         // Forum with tracking off.
689         $record = new stdClass();
690         $record->course = $course->id;
691         $forum = self::getDataGenerator()->create_module('forum', $record);
692         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
693         $forumcontext = context_module::instance($forum->cmid);
695         // Add discussions to the forums.
696         $record = new stdClass();
697         $record->course = $course->id;
698         $record->userid = $user->id;
699         $record->forum = $forum->id;
700         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
702         // Try to post (user not enrolled).
703         self::setUser($user);
704         try {
705             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
706             $this->fail('Exception expected due to being unenrolled from the course.');
707         } catch (moodle_exception $e) {
708             $this->assertEquals('requireloginerror', $e->errorcode);
709         }
711         $this->getDataGenerator()->enrol_user($user->id, $course->id);
712         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
714         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
715         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
717         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
718         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
719         // We receive the discussion and the post.
720         $this->assertEquals(2, count($posts['posts']));
722         $tested = false;
723         foreach ($posts['posts'] as $thispost) {
724             if ($createdpost['postid'] == $thispost['id']) {
725                 $this->assertEquals('some subject', $thispost['subject']);
726                 $this->assertEquals('some text here...', $thispost['message']);
727                 $tested = true;
728             }
729         }
730         $this->assertTrue($tested);
732         // Test inline and regular attachment in post
733         // Create a file in a draft area for inline attachments.
734         $draftidinlineattach = file_get_unused_draft_itemid();
735         $draftidattach = file_get_unused_draft_itemid();
736         self::setUser($user);
737         $usercontext = context_user::instance($user->id);
738         $filepath = '/';
739         $filearea = 'draft';
740         $component = 'user';
741         $filenameimg = 'shouldbeanimage.txt';
742         $filerecordinline = array(
743             'contextid' => $usercontext->id,
744             'component' => $component,
745             'filearea'  => $filearea,
746             'itemid'    => $draftidinlineattach,
747             'filepath'  => $filepath,
748             'filename'  => $filenameimg,
749         );
750         $fs = get_file_storage();
752         // Create a file in a draft area for regular attachments.
753         $filerecordattach = $filerecordinline;
754         $attachfilename = 'attachment.txt';
755         $filerecordattach['filename'] = $attachfilename;
756         $filerecordattach['itemid'] = $draftidattach;
757         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
758         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
760         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
761                          array('name' => 'attachmentsid', 'value' => $draftidattach));
762         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot
763                      . "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}"
764                      . '" alt="inlineimage">.';
765         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment',
766                                                                $dummytext, $options);
767         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
769         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
770         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
771         // We receive the discussion and the post.
772         // Can't guarantee order of posts during tests.
773         $postfound = false;
774         foreach ($posts['posts'] as $thispost) {
775             if ($createdpost['postid'] == $thispost['id']) {
776                 $this->assertEquals($createdpost['postid'], $thispost['id']);
777                 $this->assertEquals($thispost['attachment'], 1, "There should be a non-inline attachment");
778                 $this->assertCount(1, $thispost['attachments'], "There should be 1 attachment");
779                 $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
780                 $this->assertContains('pluginfile.php', $thispost['message']);
781                 $postfound = true;
782                 break;
783             }
784         }
786         $this->assertTrue($postfound);
788         // Check not posting in groups the user is not member of.
789         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
790         groups_add_member($group->id, $otheruser->id);
792         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
793         $record->forum = $forum->id;
794         $record->userid = $otheruser->id;
795         $record->groupid = $group->id;
796         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
798         try {
799             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
800             $this->fail('Exception expected due to invalid permissions for posting.');
801         } catch (moodle_exception $e) {
802             $this->assertEquals('nopostforum', $e->errorcode);
803         }
805     }
807     /*
808      * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
809      */
810     public function test_add_discussion() {
811         global $CFG, $USER;
812         $this->resetAfterTest(true);
814         // Create courses to add the modules.
815         $course = self::getDataGenerator()->create_course();
817         $user1 = self::getDataGenerator()->create_user();
818         $user2 = self::getDataGenerator()->create_user();
820         // First forum with tracking off.
821         $record = new stdClass();
822         $record->course = $course->id;
823         $record->type = 'news';
824         $forum = self::getDataGenerator()->create_module('forum', $record);
826         self::setUser($user1);
827         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
829         try {
830             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
831             $this->fail('Exception expected due to invalid permissions.');
832         } catch (moodle_exception $e) {
833             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
834         }
836         self::setAdminUser();
837         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
838         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
840         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
841         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
843         $this->assertCount(1, $discussions['discussions']);
844         $this->assertCount(0, $discussions['warnings']);
846         $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']);
847         $this->assertEquals(-1, $discussions['discussions'][0]['groupid']);
848         $this->assertEquals('the subject', $discussions['discussions'][0]['subject']);
849         $this->assertEquals('some text here...', $discussions['discussions'][0]['message']);
851         $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1,
852                                                                 array('options' => array('name' => 'discussionpinned',
853                                                                                          'value' => true)));
854         $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...');
855         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
856         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
857         $this->assertCount(3, $discussions['discussions']);
858         $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']);
860         // Test inline and regular attachment in new discussion
861         // Create a file in a draft area for inline attachments.
863         $fs = get_file_storage();
865         $draftidinlineattach = file_get_unused_draft_itemid();
866         $draftidattach = file_get_unused_draft_itemid();
868         $usercontext = context_user::instance($USER->id);
869         $filepath = '/';
870         $filearea = 'draft';
871         $component = 'user';
872         $filenameimg = 'shouldbeanimage.txt';
873         $filerecord = array(
874             'contextid' => $usercontext->id,
875             'component' => $component,
876             'filearea'  => $filearea,
877             'itemid'    => $draftidinlineattach,
878             'filepath'  => $filepath,
879             'filename'  => $filenameimg,
880         );
882         // Create a file in a draft area for regular attachments.
883         $filerecordattach = $filerecord;
884         $attachfilename = 'attachment.txt';
885         $filerecordattach['filename'] = $attachfilename;
886         $filerecordattach['itemid'] = $draftidattach;
887         $fs->create_file_from_string($filerecord, 'image contents (not really)');
888         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
890         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
891                     "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
892                     '" alt="inlineimage">.';
894         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
895                          array('name' => 'attachmentsid', 'value' => $draftidattach));
896         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject',
897                                                                 $dummytext, -1, $options);
898         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
900         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
901         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
903         $this->assertCount(4, $discussions['discussions']);
904         $this->assertCount(0, $createddiscussion['warnings']);
905         // Can't guarantee order of posts during tests.
906         $postfound = false;
907         foreach ($discussions['discussions'] as $thisdiscussion) {
908             if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) {
909                 $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment");
910                 $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment");
911                 $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
912                 $this->assertNotContains('draftfile.php', $thisdiscussion['message']);
913                 $this->assertContains('pluginfile.php', $thisdiscussion['message']);
914                 $postfound = true;
915                 break;
916             }
917         }
919         $this->assertTrue($postfound);
920     }
922     /**
923      * Test adding discussions in a course with gorups
924      */
925     public function test_add_discussion_in_course_with_groups() {
926         global $CFG;
928         $this->resetAfterTest(true);
930         // Create course to add the module.
931         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
932         $user = self::getDataGenerator()->create_user();
933         $this->getDataGenerator()->enrol_user($user->id, $course->id);
935         // Forum forcing separate gropus.
936         $record = new stdClass();
937         $record->course = $course->id;
938         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
940         // Try to post (user not enrolled).
941         self::setUser($user);
943         // The user is not enroled in any group, try to post in a forum with separate groups.
944         try {
945             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
946             $this->fail('Exception expected due to invalid group permissions.');
947         } catch (moodle_exception $e) {
948             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
949         }
951         try {
952             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0);
953             $this->fail('Exception expected due to invalid group permissions.');
954         } catch (moodle_exception $e) {
955             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
956         }
958         // Create a group.
959         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
961         // Try to post in a group the user is not enrolled.
962         try {
963             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
964             $this->fail('Exception expected due to invalid group permissions.');
965         } catch (moodle_exception $e) {
966             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
967         }
969         // Add the user to a group.
970         groups_add_member($group->id, $user->id);
972         // Try to post in a group the user is not enrolled.
973         try {
974             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1);
975             $this->fail('Exception expected due to invalid group.');
976         } catch (moodle_exception $e) {
977             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
978         }
980         // Nost add the discussion using a valid group.
981         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
982         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
984         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
985         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
987         $this->assertCount(1, $discussions['discussions']);
988         $this->assertCount(0, $discussions['warnings']);
989         $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']);
990         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
992         // Now add a discussions without indicating a group. The function should guess the correct group.
993         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
994         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
996         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
997         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
999         $this->assertCount(2, $discussions['discussions']);
1000         $this->assertCount(0, $discussions['warnings']);
1001         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1002         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1004         // Enrol the same user in other group.
1005         $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1006         groups_add_member($group2->id, $user->id);
1008         // Now add a discussions without indicating a group. The function should guess the correct group (the first one).
1009         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1010         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1012         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1013         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1015         $this->assertCount(3, $discussions['discussions']);
1016         $this->assertCount(0, $discussions['warnings']);
1017         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1018         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1019         $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']);
1021     }
1023     /*
1024      * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests.
1025      */
1026     public function test_can_add_discussion() {
1027         global $DB;
1028         $this->resetAfterTest(true);
1030         // Create courses to add the modules.
1031         $course = self::getDataGenerator()->create_course();
1033         $user = self::getDataGenerator()->create_user();
1035         // First forum with tracking off.
1036         $record = new stdClass();
1037         $record->course = $course->id;
1038         $record->type = 'news';
1039         $forum = self::getDataGenerator()->create_module('forum', $record);
1041         // User with no permissions to add in a news forum.
1042         self::setUser($user);
1043         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1045         $result = mod_forum_external::can_add_discussion($forum->id);
1046         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1047         $this->assertFalse($result['status']);
1048         $this->assertFalse($result['canpindiscussions']);
1049         $this->assertTrue($result['cancreateattachment']);
1051         // Disable attachments.
1052         $DB->set_field('forum', 'maxattachments', 0, array('id' => $forum->id));
1053         $result = mod_forum_external::can_add_discussion($forum->id);
1054         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1055         $this->assertFalse($result['status']);
1056         $this->assertFalse($result['canpindiscussions']);
1057         $this->assertFalse($result['cancreateattachment']);
1058         $DB->set_field('forum', 'maxattachments', 1, array('id' => $forum->id));    // Enable attachments again.
1060         self::setAdminUser();
1061         $result = mod_forum_external::can_add_discussion($forum->id);
1062         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
1063         $this->assertTrue($result['status']);
1064         $this->assertTrue($result['canpindiscussions']);
1065         $this->assertTrue($result['cancreateattachment']);
1067     }