MDL-55982 mod_forum: Add time-based discussion locking
[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');
32 class mod_forum_external_testcase extends externallib_advanced_testcase {
34     /**
35      * Tests set up
36      */
37     protected function setUp() {
38         global $CFG;
40         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
41         // tests using these functions.
42         \mod_forum\subscriptions::reset_forum_cache();
44         require_once($CFG->dirroot . '/mod/forum/externallib.php');
45     }
47     public function tearDown() {
48         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
49         // tests using these functions.
50         \mod_forum\subscriptions::reset_forum_cache();
51     }
53     /**
54      * Test get forums
55      */
56     public function test_mod_forum_get_forums_by_courses() {
57         global $USER, $CFG, $DB;
59         $this->resetAfterTest(true);
61         // Create a user.
62         $user = self::getDataGenerator()->create_user();
64         // Set to the user.
65         self::setUser($user);
67         // Create courses to add the modules.
68         $course1 = self::getDataGenerator()->create_course();
69         $course2 = self::getDataGenerator()->create_course();
71         // First forum.
72         $record = new stdClass();
73         $record->introformat = FORMAT_HTML;
74         $record->course = $course1->id;
75         $forum1 = self::getDataGenerator()->create_module('forum', $record);
77         // Second forum.
78         $record = new stdClass();
79         $record->introformat = FORMAT_HTML;
80         $record->course = $course2->id;
81         $forum2 = self::getDataGenerator()->create_module('forum', $record);
82         $forum2->introfiles = [];
84         // Add discussions to the forums.
85         $record = new stdClass();
86         $record->course = $course1->id;
87         $record->userid = $user->id;
88         $record->forum = $forum1->id;
89         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
90         // Expect one discussion.
91         $forum1->numdiscussions = 1;
92         $forum1->cancreatediscussions = true;
93         $forum1->introfiles = [];
95         $record = new stdClass();
96         $record->course = $course2->id;
97         $record->userid = $user->id;
98         $record->forum = $forum2->id;
99         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
100         $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
101         // Expect two discussions.
102         $forum2->numdiscussions = 2;
103         // Default limited role, no create discussion capability enabled.
104         $forum2->cancreatediscussions = false;
106         // Check the forum was correctly created.
107         $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2',
108                 array('forum1' => $forum1->id, 'forum2' => $forum2->id)));
110         // Enrol the user in two courses.
111         // DataGenerator->enrol_user automatically sets a role for the user with the permission mod/form:viewdiscussion.
112         $this->getDataGenerator()->enrol_user($user->id, $course1->id, null, 'manual');
113         // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
114         $enrol = enrol_get_plugin('manual');
115         $enrolinstances = enrol_get_instances($course2->id, true);
116         foreach ($enrolinstances as $courseenrolinstance) {
117             if ($courseenrolinstance->enrol == "manual") {
118                 $instance2 = $courseenrolinstance;
119                 break;
120             }
121         }
122         $enrol->enrol_user($instance2, $user->id);
124         // Assign capabilities to view forums for forum 2.
125         $cm2 = get_coursemodule_from_id('forum', $forum2->cmid, 0, false, MUST_EXIST);
126         $context2 = context_module::instance($cm2->id);
127         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
128         $roleid2 = $this->assignUserCapability('mod/forum:viewdiscussion', $context2->id, $newrole);
130         // Create what we expect to be returned when querying the two courses.
131         unset($forum1->displaywordcount);
132         unset($forum2->displaywordcount);
134         $expectedforums = array();
135         $expectedforums[$forum1->id] = (array) $forum1;
136         $expectedforums[$forum2->id] = (array) $forum2;
138         // Call the external function passing course ids.
139         $forums = mod_forum_external::get_forums_by_courses(array($course1->id, $course2->id));
140         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
141         $this->assertCount(2, $forums);
142         foreach ($forums as $forum) {
143             $this->assertEquals($expectedforums[$forum['id']], $forum);
144         }
146         // Call the external function without passing course id.
147         $forums = mod_forum_external::get_forums_by_courses();
148         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
149         $this->assertCount(2, $forums);
150         foreach ($forums as $forum) {
151             $this->assertEquals($expectedforums[$forum['id']], $forum);
152         }
154         // Unenrol user from second course and alter expected forums.
155         $enrol->unenrol_user($instance2, $user->id);
156         unset($expectedforums[$forum2->id]);
158         // Call the external function without passing course id.
159         $forums = mod_forum_external::get_forums_by_courses();
160         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
161         $this->assertCount(1, $forums);
162         $this->assertEquals($expectedforums[$forum1->id], $forums[0]);
163         $this->assertTrue($forums[0]['cancreatediscussions']);
165         // Change the type of the forum, the user shouldn't be able to add discussions.
166         $DB->set_field('forum', 'type', 'news', array('id' => $forum1->id));
167         $forums = mod_forum_external::get_forums_by_courses();
168         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
169         $this->assertFalse($forums[0]['cancreatediscussions']);
171         // Call for the second course we unenrolled the user from.
172         $forums = mod_forum_external::get_forums_by_courses(array($course2->id));
173         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
174         $this->assertCount(0, $forums);
175     }
177     /**
178      * Test get forum posts
179      */
180     public function test_mod_forum_get_forum_discussion_posts() {
181         global $CFG, $PAGE;
183         $this->resetAfterTest(true);
185         // Set the CFG variable to allow track forums.
186         $CFG->forum_trackreadposts = true;
188         // Create a user who can track forums.
189         $record = new stdClass();
190         $record->trackforums = true;
191         $user1 = self::getDataGenerator()->create_user($record);
192         // Create a bunch of other users to post.
193         $user2 = self::getDataGenerator()->create_user();
194         $user3 = self::getDataGenerator()->create_user();
196         // Set the first created user to the test user.
197         self::setUser($user1);
199         // Create course to add the module.
200         $course1 = self::getDataGenerator()->create_course();
202         // Forum with tracking off.
203         $record = new stdClass();
204         $record->course = $course1->id;
205         $record->trackingtype = FORUM_TRACKING_OFF;
206         $forum1 = self::getDataGenerator()->create_module('forum', $record);
207         $forum1context = context_module::instance($forum1->cmid);
209         // Add discussions to the forums.
210         $record = new stdClass();
211         $record->course = $course1->id;
212         $record->userid = $user1->id;
213         $record->forum = $forum1->id;
214         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
216         $record = new stdClass();
217         $record->course = $course1->id;
218         $record->userid = $user2->id;
219         $record->forum = $forum1->id;
220         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
222         // Add 2 replies to the discussion 1 from different users.
223         $record = new stdClass();
224         $record->discussion = $discussion1->id;
225         $record->parent = $discussion1->firstpost;
226         $record->userid = $user2->id;
227         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
229         $record->parent = $discussion1reply1->id;
230         $record->userid = $user3->id;
231         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
233         // Enrol the user in the  course.
234         $enrol = enrol_get_plugin('manual');
235         // Following line enrol and assign default role id to the user.
236         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
237         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
238         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
240         // Delete one user, to test that we still receive posts by this user.
241         delete_user($user3);
243         // Create what we expect to be returned when querying the discussion.
244         $expectedposts = array(
245             'posts' => array(),
246             'warnings' => array(),
247         );
249         // User pictures are initially empty, we should get the links once the external function is called.
250         $expectedposts['posts'][] = array(
251             'id' => $discussion1reply2->id,
252             'discussion' => $discussion1reply2->discussion,
253             'parent' => $discussion1reply2->parent,
254             'userid' => (int) $discussion1reply2->userid,
255             'created' => $discussion1reply2->created,
256             'modified' => $discussion1reply2->modified,
257             'mailed' => $discussion1reply2->mailed,
258             'subject' => $discussion1reply2->subject,
259             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
260                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
261             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
262             'messagetrust' => $discussion1reply2->messagetrust,
263             'attachment' => $discussion1reply2->attachment,
264             'totalscore' => $discussion1reply2->totalscore,
265             'mailnow' => $discussion1reply2->mailnow,
266             'children' => array(),
267             'canreply' => true,
268             'postread' => false,
269             'userfullname' => fullname($user3),
270             'userpictureurl' => ''
271         );
273         $expectedposts['posts'][] = array(
274             'id' => $discussion1reply1->id,
275             'discussion' => $discussion1reply1->discussion,
276             'parent' => $discussion1reply1->parent,
277             'userid' => (int) $discussion1reply1->userid,
278             'created' => $discussion1reply1->created,
279             'modified' => $discussion1reply1->modified,
280             'mailed' => $discussion1reply1->mailed,
281             'subject' => $discussion1reply1->subject,
282             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
283                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
284             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
285             'messagetrust' => $discussion1reply1->messagetrust,
286             'attachment' => $discussion1reply1->attachment,
287             'totalscore' => $discussion1reply1->totalscore,
288             'mailnow' => $discussion1reply1->mailnow,
289             'children' => array($discussion1reply2->id),
290             'canreply' => true,
291             'postread' => false,
292             'userfullname' => fullname($user2),
293             'userpictureurl' => ''
294         );
296         // Test a discussion with two additional posts (total 3 posts).
297         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
298         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
299         $this->assertEquals(3, count($posts['posts']));
301         // Generate here the pictures because we need to wait to the external function to init the theme.
302         $userpicture = new user_picture($user3);
303         $userpicture->size = 1; // Size f1.
304         $expectedposts['posts'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
306         $userpicture = new user_picture($user2);
307         $userpicture->size = 1; // Size f1.
308         $expectedposts['posts'][1]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
310         // Unset the initial discussion post.
311         array_pop($posts['posts']);
312         $this->assertEquals($expectedposts, $posts);
314         // Test discussion without additional posts. There should be only one post (the one created by the discussion).
315         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id, 'modified', 'DESC');
316         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
317         $this->assertEquals(1, count($posts['posts']));
319     }
321     /**
322      * Test get forum posts (qanda forum)
323      */
324     public function test_mod_forum_get_forum_discussion_posts_qanda() {
325         global $CFG, $DB;
327         $this->resetAfterTest(true);
329         $record = new stdClass();
330         $user1 = self::getDataGenerator()->create_user($record);
331         $user2 = self::getDataGenerator()->create_user();
333         // Set the first created user to the test user.
334         self::setUser($user1);
336         // Create course to add the module.
337         $course1 = self::getDataGenerator()->create_course();
338         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
339         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
341         // Forum with tracking off.
342         $record = new stdClass();
343         $record->course = $course1->id;
344         $record->type = 'qanda';
345         $forum1 = self::getDataGenerator()->create_module('forum', $record);
346         $forum1context = context_module::instance($forum1->cmid);
348         // Add discussions to the forums.
349         $record = new stdClass();
350         $record->course = $course1->id;
351         $record->userid = $user2->id;
352         $record->forum = $forum1->id;
353         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
355         // Add 1 reply (not the actual user).
356         $record = new stdClass();
357         $record->discussion = $discussion1->id;
358         $record->parent = $discussion1->firstpost;
359         $record->userid = $user2->id;
360         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
362         // We still see only the original post.
363         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
364         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
365         $this->assertEquals(1, count($posts['posts']));
367         // Add a new reply, the user is going to be able to see only the original post and their new post.
368         $record = new stdClass();
369         $record->discussion = $discussion1->id;
370         $record->parent = $discussion1->firstpost;
371         $record->userid = $user1->id;
372         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
374         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
375         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
376         $this->assertEquals(2, count($posts['posts']));
378         // Now, we can fake the time of the user post, so he can se the rest of the discussion posts.
379         $discussion1reply2->created -= $CFG->maxeditingtime * 2;
380         $DB->update_record('forum_posts', $discussion1reply2);
382         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
383         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
384         $this->assertEquals(3, count($posts['posts']));
385     }
387     /**
388      * Test get forum discussions paginated
389      */
390     public function test_mod_forum_get_forum_discussions_paginated() {
391         global $USER, $CFG, $DB, $PAGE;
393         $this->resetAfterTest(true);
395         // Set the CFG variable to allow track forums.
396         $CFG->forum_trackreadposts = true;
398         // Create a user who can track forums.
399         $record = new stdClass();
400         $record->trackforums = true;
401         $user1 = self::getDataGenerator()->create_user($record);
402         // Create a bunch of other users to post.
403         $user2 = self::getDataGenerator()->create_user();
404         $user3 = self::getDataGenerator()->create_user();
405         $user4 = self::getDataGenerator()->create_user();
407         // Set the first created user to the test user.
408         self::setUser($user1);
410         // Create courses to add the modules.
411         $course1 = self::getDataGenerator()->create_course();
413         // First forum with tracking off.
414         $record = new stdClass();
415         $record->course = $course1->id;
416         $record->trackingtype = FORUM_TRACKING_OFF;
417         $forum1 = self::getDataGenerator()->create_module('forum', $record);
419         // Add discussions to the forums.
420         $record = new stdClass();
421         $record->course = $course1->id;
422         $record->userid = $user1->id;
423         $record->forum = $forum1->id;
424         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
426         // Add three replies to the discussion 1 from different users.
427         $record = new stdClass();
428         $record->discussion = $discussion1->id;
429         $record->parent = $discussion1->firstpost;
430         $record->userid = $user2->id;
431         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
433         $record->parent = $discussion1reply1->id;
434         $record->userid = $user3->id;
435         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
437         $record->userid = $user4->id;
438         $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
440         // Enrol the user in the first course.
441         $enrol = enrol_get_plugin('manual');
443         // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
444         $enrolinstances = enrol_get_instances($course1->id, true);
445         foreach ($enrolinstances as $courseenrolinstance) {
446             if ($courseenrolinstance->enrol == "manual") {
447                 $instance1 = $courseenrolinstance;
448                 break;
449             }
450         }
451         $enrol->enrol_user($instance1, $user1->id);
453         // Delete one user.
454         delete_user($user4);
456         // Assign capabilities to view discussions for forum 1.
457         $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
458         $context = context_module::instance($cm->id);
459         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
460         $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
462         // Create what we expect to be returned when querying the forums.
464         $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
466         // User pictures are initially empty, we should get the links once the external function is called.
467         $expecteddiscussions = array(
468                 'id' => $discussion1->firstpost,
469                 'name' => $discussion1->name,
470                 'groupid' => $discussion1->groupid,
471                 'timemodified' => $discussion1reply3->created,
472                 'usermodified' => $discussion1reply3->userid,
473                 'timestart' => $discussion1->timestart,
474                 'timeend' => $discussion1->timeend,
475                 'discussion' => $discussion1->id,
476                 'parent' => 0,
477                 'userid' => $discussion1->userid,
478                 'created' => $post1->created,
479                 'modified' => $post1->modified,
480                 'mailed' => $post1->mailed,
481                 'subject' => $post1->subject,
482                 'message' => $post1->message,
483                 'messageformat' => $post1->messageformat,
484                 'messagetrust' => $post1->messagetrust,
485                 'attachment' => $post1->attachment,
486                 'totalscore' => $post1->totalscore,
487                 'mailnow' => $post1->mailnow,
488                 'userfullname' => fullname($user1),
489                 'usermodifiedfullname' => fullname($user4),
490                 'userpictureurl' => '',
491                 'usermodifiedpictureurl' => '',
492                 'numreplies' => 3,
493                 'numunread' => 0,
494                 'pinned' => FORUM_DISCUSSION_UNPINNED,
495                 'locked' => false,
496                 'canreply' => false,
497             );
499         // Call the external function passing forum id.
500         $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
501         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
502         $expectedreturn = array(
503             'discussions' => array($expecteddiscussions),
504             'warnings' => array()
505         );
507         // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
508         $userpicture = new user_picture($user1);
509         $userpicture->size = 1; // Size f1.
510         $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
512         $userpicture = new user_picture($user4);
513         $userpicture->size = 1; // Size f1.
514         $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
516         $this->assertEquals($expectedreturn, $discussions);
518         // Call without required view discussion capability.
519         $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
520         try {
521             mod_forum_external::get_forum_discussions_paginated($forum1->id);
522             $this->fail('Exception expected due to missing capability.');
523         } catch (moodle_exception $e) {
524             $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
525         }
527         // Unenrol user from second course.
528         $enrol->unenrol_user($instance1, $user1->id);
530         // Call for the second course we unenrolled the user from, make sure exception thrown.
531         try {
532             mod_forum_external::get_forum_discussions_paginated($forum1->id);
533             $this->fail('Exception expected due to being unenrolled from the course.');
534         } catch (moodle_exception $e) {
535             $this->assertEquals('requireloginerror', $e->errorcode);
536         }
537     }
539     /**
540      * Test get forum discussions paginated (qanda forums)
541      */
542     public function test_mod_forum_get_forum_discussions_paginated_qanda() {
544         $this->resetAfterTest(true);
546         // Create courses to add the modules.
547         $course = self::getDataGenerator()->create_course();
549         $user1 = self::getDataGenerator()->create_user();
550         $user2 = self::getDataGenerator()->create_user();
552         // First forum with tracking off.
553         $record = new stdClass();
554         $record->course = $course->id;
555         $record->type = 'qanda';
556         $forum = self::getDataGenerator()->create_module('forum', $record);
558         // Add discussions to the forums.
559         $discussionrecord = new stdClass();
560         $discussionrecord->course = $course->id;
561         $discussionrecord->userid = $user2->id;
562         $discussionrecord->forum = $forum->id;
563         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
565         self::setAdminUser();
566         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
567         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
569         $this->assertCount(1, $discussions['discussions']);
570         $this->assertCount(0, $discussions['warnings']);
572         self::setUser($user1);
573         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
575         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
576         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
578         $this->assertCount(1, $discussions['discussions']);
579         $this->assertCount(0, $discussions['warnings']);
581     }
583     /**
584      * Test add_discussion_post
585      */
586     public function test_add_discussion_post() {
587         global $CFG;
589         $this->resetAfterTest(true);
591         $user = self::getDataGenerator()->create_user();
592         $otheruser = self::getDataGenerator()->create_user();
594         self::setAdminUser();
596         // Create course to add the module.
597         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
599         // Forum with tracking off.
600         $record = new stdClass();
601         $record->course = $course->id;
602         $forum = self::getDataGenerator()->create_module('forum', $record);
603         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
604         $forumcontext = context_module::instance($forum->cmid);
606         // Add discussions to the forums.
607         $record = new stdClass();
608         $record->course = $course->id;
609         $record->userid = $user->id;
610         $record->forum = $forum->id;
611         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
613         // Try to post (user not enrolled).
614         self::setUser($user);
615         try {
616             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
617             $this->fail('Exception expected due to being unenrolled from the course.');
618         } catch (moodle_exception $e) {
619             $this->assertEquals('requireloginerror', $e->errorcode);
620         }
622         $this->getDataGenerator()->enrol_user($user->id, $course->id);
623         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
625         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
626         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
628         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
629         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
630         // We receive the discussion and the post.
631         $this->assertEquals(2, count($posts['posts']));
633         $tested = false;
634         foreach ($posts['posts'] as $thispost) {
635             if ($createdpost['postid'] == $thispost['id']) {
636                 $this->assertEquals('some subject', $thispost['subject']);
637                 $this->assertEquals('some text here...', $thispost['message']);
638                 $tested = true;
639             }
640         }
641         $this->assertTrue($tested);
643         // Test inline and regular attachment in post
644         // Create a file in a draft area for inline attachments.
645         $draftidinlineattach = file_get_unused_draft_itemid();
646         $draftidattach = file_get_unused_draft_itemid();
647         self::setUser($user);
648         $usercontext = context_user::instance($user->id);
649         $filepath = '/';
650         $filearea = 'draft';
651         $component = 'user';
652         $filenameimg = 'shouldbeanimage.txt';
653         $filerecordinline = array(
654             'contextid' => $usercontext->id,
655             'component' => $component,
656             'filearea'  => $filearea,
657             'itemid'    => $draftidinlineattach,
658             'filepath'  => $filepath,
659             'filename'  => $filenameimg,
660         );
661         $fs = get_file_storage();
663         // Create a file in a draft area for regular attachments.
664         $filerecordattach = $filerecordinline;
665         $attachfilename = 'attachment.txt';
666         $filerecordattach['filename'] = $attachfilename;
667         $filerecordattach['itemid'] = $draftidattach;
668         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
669         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
671         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
672                          array('name' => 'attachmentsid', 'value' => $draftidattach));
673         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot
674                      . "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}"
675                      . '" alt="inlineimage">.';
676         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment',
677                                                                $dummytext, $options);
678         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
680         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
681         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
682         // We receive the discussion and the post.
683         // Can't guarantee order of posts during tests.
684         $postfound = false;
685         foreach ($posts['posts'] as $thispost) {
686             if ($createdpost['postid'] == $thispost['id']) {
687                 $this->assertEquals($createdpost['postid'], $thispost['id']);
688                 $this->assertEquals($thispost['attachment'], 1, "There should be a non-inline attachment");
689                 $this->assertCount(1, $thispost['attachments'], "There should be 1 attachment");
690                 $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
691                 $this->assertContains('pluginfile.php', $thispost['message']);
692                 $postfound = true;
693                 break;
694             }
695         }
697         $this->assertTrue($postfound);
699         // Check not posting in groups the user is not member of.
700         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
701         groups_add_member($group->id, $otheruser->id);
703         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
704         $record->forum = $forum->id;
705         $record->userid = $otheruser->id;
706         $record->groupid = $group->id;
707         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
709         try {
710             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
711             $this->fail('Exception expected due to invalid permissions for posting.');
712         } catch (moodle_exception $e) {
713             $this->assertEquals('nopostforum', $e->errorcode);
714         }
716     }
718     /*
719      * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
720      */
721     public function test_add_discussion() {
722         global $CFG, $USER;
723         $this->resetAfterTest(true);
725         // Create courses to add the modules.
726         $course = self::getDataGenerator()->create_course();
728         $user1 = self::getDataGenerator()->create_user();
729         $user2 = self::getDataGenerator()->create_user();
731         // First forum with tracking off.
732         $record = new stdClass();
733         $record->course = $course->id;
734         $record->type = 'news';
735         $forum = self::getDataGenerator()->create_module('forum', $record);
737         self::setUser($user1);
738         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
740         try {
741             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
742             $this->fail('Exception expected due to invalid permissions.');
743         } catch (moodle_exception $e) {
744             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
745         }
747         self::setAdminUser();
748         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
749         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
751         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
752         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
754         $this->assertCount(1, $discussions['discussions']);
755         $this->assertCount(0, $discussions['warnings']);
757         $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']);
758         $this->assertEquals(-1, $discussions['discussions'][0]['groupid']);
759         $this->assertEquals('the subject', $discussions['discussions'][0]['subject']);
760         $this->assertEquals('some text here...', $discussions['discussions'][0]['message']);
762         $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1,
763                                                                 array('options' => array('name' => 'discussionpinned',
764                                                                                          'value' => true)));
765         $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...');
766         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
767         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
768         $this->assertCount(3, $discussions['discussions']);
769         $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']);
771         // Test inline and regular attachment in new discussion
772         // Create a file in a draft area for inline attachments.
774         $fs = get_file_storage();
776         $draftidinlineattach = file_get_unused_draft_itemid();
777         $draftidattach = file_get_unused_draft_itemid();
779         $usercontext = context_user::instance($USER->id);
780         $filepath = '/';
781         $filearea = 'draft';
782         $component = 'user';
783         $filenameimg = 'shouldbeanimage.txt';
784         $filerecord = array(
785             'contextid' => $usercontext->id,
786             'component' => $component,
787             'filearea'  => $filearea,
788             'itemid'    => $draftidinlineattach,
789             'filepath'  => $filepath,
790             'filename'  => $filenameimg,
791         );
793         // Create a file in a draft area for regular attachments.
794         $filerecordattach = $filerecord;
795         $attachfilename = 'attachment.txt';
796         $filerecordattach['filename'] = $attachfilename;
797         $filerecordattach['itemid'] = $draftidattach;
798         $fs->create_file_from_string($filerecord, 'image contents (not really)');
799         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
801         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
802                     "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
803                     '" alt="inlineimage">.';
805         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
806                          array('name' => 'attachmentsid', 'value' => $draftidattach));
807         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject',
808                                                                 $dummytext, -1, $options);
809         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
811         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
812         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
814         $this->assertCount(4, $discussions['discussions']);
815         $this->assertCount(0, $createddiscussion['warnings']);
816         // Can't guarantee order of posts during tests.
817         $postfound = false;
818         foreach ($discussions['discussions'] as $thisdiscussion) {
819             if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) {
820                 $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment");
821                 $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment");
822                 $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
823                 $this->assertNotContains('draftfile.php', $thisdiscussion['message']);
824                 $this->assertContains('pluginfile.php', $thisdiscussion['message']);
825                 $postfound = true;
826                 break;
827             }
828         }
830         $this->assertTrue($postfound);
831     }
833     /**
834      * Test adding discussions in a course with gorups
835      */
836     public function test_add_discussion_in_course_with_groups() {
837         global $CFG;
839         $this->resetAfterTest(true);
841         // Create course to add the module.
842         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
843         $user = self::getDataGenerator()->create_user();
844         $this->getDataGenerator()->enrol_user($user->id, $course->id);
846         // Forum forcing separate gropus.
847         $record = new stdClass();
848         $record->course = $course->id;
849         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
851         // Try to post (user not enrolled).
852         self::setUser($user);
854         // The user is not enroled in any group, try to post in a forum with separate groups.
855         try {
856             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
857             $this->fail('Exception expected due to invalid group permissions.');
858         } catch (moodle_exception $e) {
859             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
860         }
862         try {
863             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0);
864             $this->fail('Exception expected due to invalid group permissions.');
865         } catch (moodle_exception $e) {
866             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
867         }
869         // Create a group.
870         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
872         // Try to post in a group the user is not enrolled.
873         try {
874             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
875             $this->fail('Exception expected due to invalid group permissions.');
876         } catch (moodle_exception $e) {
877             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
878         }
880         // Add the user to a group.
881         groups_add_member($group->id, $user->id);
883         // Try to post in a group the user is not enrolled.
884         try {
885             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1);
886             $this->fail('Exception expected due to invalid group.');
887         } catch (moodle_exception $e) {
888             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
889         }
891         // Nost add the discussion using a valid group.
892         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
893         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
895         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
896         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
898         $this->assertCount(1, $discussions['discussions']);
899         $this->assertCount(0, $discussions['warnings']);
900         $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']);
901         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
903         // Now add a discussions without indicating a group. The function should guess the correct group.
904         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
905         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
907         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
908         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
910         $this->assertCount(2, $discussions['discussions']);
911         $this->assertCount(0, $discussions['warnings']);
912         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
913         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
915         // Enrol the same user in other group.
916         $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
917         groups_add_member($group2->id, $user->id);
919         // Now add a discussions without indicating a group. The function should guess the correct group (the first one).
920         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
921         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
923         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
924         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
926         $this->assertCount(3, $discussions['discussions']);
927         $this->assertCount(0, $discussions['warnings']);
928         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
929         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
930         $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']);
932     }
934     /*
935      * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests.
936      */
937     public function test_can_add_discussion() {
939         $this->resetAfterTest(true);
941         // Create courses to add the modules.
942         $course = self::getDataGenerator()->create_course();
944         $user = self::getDataGenerator()->create_user();
946         // First forum with tracking off.
947         $record = new stdClass();
948         $record->course = $course->id;
949         $record->type = 'news';
950         $forum = self::getDataGenerator()->create_module('forum', $record);
952         // User with no permissions to add in a news forum.
953         self::setUser($user);
954         $this->getDataGenerator()->enrol_user($user->id, $course->id);
956         $result = mod_forum_external::can_add_discussion($forum->id);
957         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
958         $this->assertFalse($result['status']);
960         self::setAdminUser();
961         $result = mod_forum_external::can_add_discussion($forum->id);
962         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
963         $this->assertTrue($result['status']);
965     }