Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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/>.
  16  
  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   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  
  30  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  31  require_once($CFG->dirroot . '/mod/forum/lib.php');
  32  
  33  class mod_forum_external_testcase extends externallib_advanced_testcase {
  34  
  35      /**
  36       * Tests set up
  37       */
  38      protected function setUp() {
  39          global $CFG;
  40  
  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();
  44  
  45          require_once($CFG->dirroot . '/mod/forum/externallib.php');
  46      }
  47  
  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      }
  53  
  54      /**
  55       * Test get forums
  56       */
  57      public function test_mod_forum_get_forums_by_courses() {
  58          global $USER, $CFG, $DB;
  59  
  60          $this->resetAfterTest(true);
  61  
  62          // Create a user.
  63          $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
  64  
  65          // Set to the user.
  66          self::setUser($user);
  67  
  68          // Create courses to add the modules.
  69          $course1 = self::getDataGenerator()->create_course();
  70          $course2 = self::getDataGenerator()->create_course();
  71  
  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);
  78  
  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 = [];
  86  
  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 = [];
  99  
 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;
 111  
 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)));
 115  
 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);
 129  
 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);
 135  
 136          // Create what we expect to be returned when querying the two courses.
 137          unset($forum1->displaywordcount);
 138          unset($forum2->displaywordcount);
 139  
 140          $expectedforums = array();
 141          $expectedforums[$forum1->id] = (array) $forum1;
 142          $expectedforums[$forum2->id] = (array) $forum2;
 143  
 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          }
 151  
 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          }
 159  
 160          // Unenrol user from second course and alter expected forums.
 161          $enrol->unenrol_user($instance2, $user->id);
 162          unset($expectedforums[$forum2->id]);
 163  
 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']);
 170  
 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']);
 176  
 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      }
 182  
 183      /**
 184       * Test the toggle favourite state
 185       */
 186      public function test_mod_forum_toggle_favourite_state() {
 187          global $USER, $CFG, $DB;
 188  
 189          $this->resetAfterTest(true);
 190  
 191          // Create a user.
 192          $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
 193  
 194          // Set to the user.
 195          self::setUser($user);
 196  
 197          // Create courses to add the modules.
 198          $course1 = self::getDataGenerator()->create_course();
 199          $this->getDataGenerator()->enrol_user($user->id, $course1->id);
 200  
 201          $record = new stdClass();
 202          $record->introformat = FORMAT_HTML;
 203          $record->course = $course1->id;
 204          $record->trackingtype = FORUM_TRACKING_OFF;
 205          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 206          $forum1->introfiles = [];
 207  
 208          // Add discussions to the forums.
 209          $record = new stdClass();
 210          $record->course = $course1->id;
 211          $record->userid = $user->id;
 212          $record->forum = $forum1->id;
 213          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 214  
 215          $response = mod_forum_external::toggle_favourite_state($discussion1->id, 1);
 216          $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response);
 217          $this->assertTrue($response['userstate']['favourited']);
 218  
 219          $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0);
 220          $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response);
 221          $this->assertFalse($response['userstate']['favourited']);
 222  
 223          $this->setUser(0);
 224          try {
 225              $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0);
 226          } catch (moodle_exception $e) {
 227              $this->assertEquals('requireloginerror', $e->errorcode);
 228          }
 229      }
 230  
 231      /**
 232       * Test the toggle pin state
 233       */
 234      public function test_mod_forum_set_pin_state() {
 235          $this->resetAfterTest(true);
 236  
 237          // Create a user.
 238          $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
 239  
 240          // Set to the user.
 241          self::setUser($user);
 242  
 243          // Create courses to add the modules.
 244          $course1 = self::getDataGenerator()->create_course();
 245          $this->getDataGenerator()->enrol_user($user->id, $course1->id);
 246  
 247          $record = new stdClass();
 248          $record->introformat = FORMAT_HTML;
 249          $record->course = $course1->id;
 250          $record->trackingtype = FORUM_TRACKING_OFF;
 251          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 252          $forum1->introfiles = [];
 253  
 254          // Add discussions to the forums.
 255          $record = new stdClass();
 256          $record->course = $course1->id;
 257          $record->userid = $user->id;
 258          $record->forum = $forum1->id;
 259          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 260  
 261          try {
 262              $response = mod_forum_external::set_pin_state($discussion1->id, 1);
 263          } catch (Exception $e) {
 264              $this->assertEquals('cannotpindiscussions', $e->errorcode);
 265          }
 266  
 267          self::setAdminUser();
 268          $response = mod_forum_external::set_pin_state($discussion1->id, 1);
 269          $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response);
 270          $this->assertTrue($response['pinned']);
 271  
 272          $response = mod_forum_external::set_pin_state($discussion1->id, 0);
 273          $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response);
 274          $this->assertFalse($response['pinned']);
 275      }
 276  
 277      /**
 278       * Test get forum posts
 279       */
 280      public function test_mod_forum_get_forum_discussion_posts() {
 281          global $CFG, $PAGE;
 282  
 283          $this->resetAfterTest(true);
 284  
 285          // Set the CFG variable to allow track forums.
 286          $CFG->forum_trackreadposts = true;
 287  
 288          // Create a user who can track forums.
 289          $record = new stdClass();
 290          $record->trackforums = true;
 291          $user1 = self::getDataGenerator()->create_user($record);
 292          // Create a bunch of other users to post.
 293          $user2 = self::getDataGenerator()->create_user();
 294          $user3 = self::getDataGenerator()->create_user();
 295  
 296          // Set the first created user to the test user.
 297          self::setUser($user1);
 298  
 299          // Create course to add the module.
 300          $course1 = self::getDataGenerator()->create_course();
 301  
 302          // Forum with tracking off.
 303          $record = new stdClass();
 304          $record->course = $course1->id;
 305          $record->trackingtype = FORUM_TRACKING_OFF;
 306          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 307          $forum1context = context_module::instance($forum1->cmid);
 308  
 309          // Forum with tracking enabled.
 310          $record = new stdClass();
 311          $record->course = $course1->id;
 312          $forum2 = self::getDataGenerator()->create_module('forum', $record);
 313          $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
 314          $forum2context = context_module::instance($forum2->cmid);
 315  
 316          // Add discussions to the forums.
 317          $record = new stdClass();
 318          $record->course = $course1->id;
 319          $record->userid = $user1->id;
 320          $record->forum = $forum1->id;
 321          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 322  
 323          $record = new stdClass();
 324          $record->course = $course1->id;
 325          $record->userid = $user2->id;
 326          $record->forum = $forum1->id;
 327          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 328  
 329          $record = new stdClass();
 330          $record->course = $course1->id;
 331          $record->userid = $user2->id;
 332          $record->forum = $forum2->id;
 333          $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 334  
 335          // Add 2 replies to the discussion 1 from different users.
 336          $record = new stdClass();
 337          $record->discussion = $discussion1->id;
 338          $record->parent = $discussion1->firstpost;
 339          $record->userid = $user2->id;
 340          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 341          $filename = 'shouldbeanimage.jpg';
 342          // Add a fake inline image to the post.
 343          $filerecordinline = array(
 344              'contextid' => $forum1context->id,
 345              'component' => 'mod_forum',
 346              'filearea'  => 'post',
 347              'itemid'    => $discussion1reply1->id,
 348              'filepath'  => '/',
 349              'filename'  => $filename,
 350          );
 351          $fs = get_file_storage();
 352          $timepost = time();
 353          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
 354  
 355          $record->parent = $discussion1reply1->id;
 356          $record->userid = $user3->id;
 357          $record->tags = array('Cats', 'Dogs');
 358          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 359  
 360          // Enrol the user in the  course.
 361          $enrol = enrol_get_plugin('manual');
 362          // Following line enrol and assign default role id to the user.
 363          // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
 364          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 365          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 366  
 367          // Delete one user, to test that we still receive posts by this user.
 368          delete_user($user3);
 369  
 370          // Create what we expect to be returned when querying the discussion.
 371          $expectedposts = array(
 372              'posts' => array(),
 373              'ratinginfo' => array(
 374                  'contextid' => $forum1context->id,
 375                  'component' => 'mod_forum',
 376                  'ratingarea' => 'post',
 377                  'canviewall' => null,
 378                  'canviewany' => null,
 379                  'scales' => array(),
 380                  'ratings' => array(),
 381              ),
 382              'warnings' => array(),
 383          );
 384  
 385          // User pictures are initially empty, we should get the links once the external function is called.
 386          $expectedposts['posts'][] = array(
 387              'id' => $discussion1reply2->id,
 388              'discussion' => $discussion1reply2->discussion,
 389              'parent' => $discussion1reply2->parent,
 390              'userid' => (int) $discussion1reply2->userid,
 391              'created' => $discussion1reply2->created,
 392              'modified' => $discussion1reply2->modified,
 393              'mailed' => $discussion1reply2->mailed,
 394              'subject' => $discussion1reply2->subject,
 395              'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
 396                      $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
 397              'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 398              'messagetrust' => $discussion1reply2->messagetrust,
 399              'attachment' => $discussion1reply2->attachment,
 400              'totalscore' => $discussion1reply2->totalscore,
 401              'mailnow' => $discussion1reply2->mailnow,
 402              'children' => array(),
 403              'canreply' => true,
 404              'postread' => false,
 405              'userfullname' => fullname($user3),
 406              'userpictureurl' => '',
 407              'deleted' => false,
 408              'isprivatereply' => false,
 409              'tags' => \core_tag\external\util::get_item_tags('mod_forum', 'forum_posts', $discussion1reply2->id),
 410          );
 411          // Cast to expected.
 412          $this->assertCount(2, $expectedposts['posts'][0]['tags']);
 413          $expectedposts['posts'][0]['tags'][0]['isstandard'] = (bool) $expectedposts['posts'][0]['tags'][0]['isstandard'];
 414          $expectedposts['posts'][0]['tags'][1]['isstandard'] = (bool) $expectedposts['posts'][0]['tags'][1]['isstandard'];
 415  
 416          $expectedposts['posts'][] = array(
 417              'id' => $discussion1reply1->id,
 418              'discussion' => $discussion1reply1->discussion,
 419              'parent' => $discussion1reply1->parent,
 420              'userid' => (int) $discussion1reply1->userid,
 421              'created' => $discussion1reply1->created,
 422              'modified' => $discussion1reply1->modified,
 423              'mailed' => $discussion1reply1->mailed,
 424              'subject' => $discussion1reply1->subject,
 425              'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
 426                      $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
 427              'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 428              'messagetrust' => $discussion1reply1->messagetrust,
 429              'attachment' => $discussion1reply1->attachment,
 430              'messageinlinefiles' => array(
 431                  array(
 432                      'filename' => $filename,
 433                      'filepath' => '/',
 434                      'filesize' => '27',
 435                      'fileurl' => moodle_url::make_webservice_pluginfile_url($forum1context->id, 'mod_forum', 'post',
 436                                      $discussion1reply1->id, '/', $filename),
 437                      'timemodified' => $timepost,
 438                      'mimetype' => 'image/jpeg',
 439                      'isexternalfile' => false,
 440                  )
 441              ),
 442              'totalscore' => $discussion1reply1->totalscore,
 443              'mailnow' => $discussion1reply1->mailnow,
 444              'children' => array($discussion1reply2->id),
 445              'canreply' => true,
 446              'postread' => false,
 447              'userfullname' => fullname($user2),
 448              'userpictureurl' => '',
 449              'deleted' => false,
 450              'isprivatereply' => false,
 451              'tags' => array(),
 452          );
 453  
 454          // Test a discussion with two additional posts (total 3 posts).
 455          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
 456          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 457          $this->assertEquals(3, count($posts['posts']));
 458  
 459          // Generate here the pictures because we need to wait to the external function to init the theme.
 460          $userpicture = new user_picture($user3);
 461          $userpicture->size = 1; // Size f1.
 462          $expectedposts['posts'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
 463  
 464          $userpicture = new user_picture($user2);
 465          $userpicture->size = 1; // Size f1.
 466          $expectedposts['posts'][1]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
 467  
 468          // Unset the initial discussion post.
 469          array_pop($posts['posts']);
 470          $this->assertEquals($expectedposts, $posts);
 471  
 472          // Check we receive the unread count correctly on tracked forum.
 473          forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
 474          $result = mod_forum_external::get_forums_by_courses(array($course1->id));
 475          $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
 476          foreach ($result as $f) {
 477              if ($f['id'] == $forum2->id) {
 478                  $this->assertEquals(1, $f['unreadpostscount']);
 479              }
 480          }
 481  
 482          // Test discussion without additional posts. There should be only one post (the one created by the discussion).
 483          $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id, 'modified', 'DESC');
 484          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 485          $this->assertEquals(1, count($posts['posts']));
 486  
 487          // Test discussion tracking on not tracked forum.
 488          $result = mod_forum_external::view_forum_discussion($discussion1->id);
 489          $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
 490          $this->assertTrue($result['status']);
 491          $this->assertEmpty($result['warnings']);
 492  
 493          // Test posts have not been marked as read.
 494          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
 495          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 496          foreach ($posts['posts'] as $post) {
 497              $this->assertFalse($post['postread']);
 498          }
 499  
 500          // Test discussion tracking on tracked forum.
 501          $result = mod_forum_external::view_forum_discussion($discussion3->id);
 502          $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
 503          $this->assertTrue($result['status']);
 504          $this->assertEmpty($result['warnings']);
 505  
 506          // Test posts have been marked as read.
 507          $posts = mod_forum_external::get_forum_discussion_posts($discussion3->id, 'modified', 'DESC');
 508          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 509          foreach ($posts['posts'] as $post) {
 510              $this->assertTrue($post['postread']);
 511          }
 512  
 513          // Check we receive 0 unread posts.
 514          forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
 515          $result = mod_forum_external::get_forums_by_courses(array($course1->id));
 516          $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
 517          foreach ($result as $f) {
 518              if ($f['id'] == $forum2->id) {
 519                  $this->assertEquals(0, $f['unreadpostscount']);
 520              }
 521          }
 522      }
 523  
 524      /**
 525       * Test get forum posts
 526       *
 527       * Tests is similar to the get_forum_discussion_posts only utilizing the new return structure and entities
 528       */
 529      public function test_mod_forum_get_discussion_posts() {
 530          global $CFG;
 531  
 532          $this->resetAfterTest(true);
 533  
 534          // Set the CFG variable to allow track forums.
 535          $CFG->forum_trackreadposts = true;
 536  
 537          $urlfactory = mod_forum\local\container::get_url_factory();
 538          $legacyfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
 539          $entityfactory = mod_forum\local\container::get_entity_factory();
 540  
 541          // Create course to add the module.
 542          $course1 = self::getDataGenerator()->create_course();
 543  
 544          // Create a user who can track forums.
 545          $record = new stdClass();
 546          $record->trackforums = true;
 547          $user1 = self::getDataGenerator()->create_user($record);
 548          // Create a bunch of other users to post.
 549          $user2 = self::getDataGenerator()->create_user();
 550          $user2entity = $entityfactory->get_author_from_stdclass($user2);
 551          $exporteduser2 = [
 552              'id' => (int) $user2->id,
 553              'fullname' => fullname($user2),
 554              'isdeleted' => false,
 555              'groups' => [],
 556              'urls' => [
 557                  'profile' => $urlfactory->get_author_profile_url($user2entity, $course1->id)->out(false),
 558                  'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
 559              ]
 560          ];
 561          $user2->fullname = $exporteduser2['fullname'];
 562  
 563          $user3 = self::getDataGenerator()->create_user(['fullname' => "Mr Pants 1"]);
 564          $user3entity = $entityfactory->get_author_from_stdclass($user3);
 565          $exporteduser3 = [
 566              'id' => (int) $user3->id,
 567              'fullname' => fullname($user3),
 568              'groups' => [],
 569              'isdeleted' => false,
 570              'urls' => [
 571                  'profile' => $urlfactory->get_author_profile_url($user3entity, $course1->id)->out(false),
 572                  'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
 573              ]
 574          ];
 575          $user3->fullname = $exporteduser3['fullname'];
 576          $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
 577  
 578          // Set the first created user to the test user.
 579          self::setUser($user1);
 580  
 581          // Forum with tracking off.
 582          $record = new stdClass();
 583          $record->course = $course1->id;
 584          $record->trackingtype = FORUM_TRACKING_OFF;
 585          // Display word count. Otherwise, word and char counts will be set to null by the forum post exporter.
 586          $record->displaywordcount = true;
 587          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 588          $forum1context = context_module::instance($forum1->cmid);
 589  
 590          // Forum with tracking enabled.
 591          $record = new stdClass();
 592          $record->course = $course1->id;
 593          $forum2 = self::getDataGenerator()->create_module('forum', $record);
 594          $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
 595          $forum2context = context_module::instance($forum2->cmid);
 596  
 597          // Add discussions to the forums.
 598          $record = new stdClass();
 599          $record->course = $course1->id;
 600          $record->userid = $user1->id;
 601          $record->forum = $forum1->id;
 602          $discussion1 = $forumgenerator->create_discussion($record);
 603  
 604          $record = new stdClass();
 605          $record->course = $course1->id;
 606          $record->userid = $user2->id;
 607          $record->forum = $forum1->id;
 608          $discussion2 = $forumgenerator->create_discussion($record);
 609  
 610          $record = new stdClass();
 611          $record->course = $course1->id;
 612          $record->userid = $user2->id;
 613          $record->forum = $forum2->id;
 614          $discussion3 = $forumgenerator->create_discussion($record);
 615  
 616          // Add 2 replies to the discussion 1 from different users.
 617          $record = new stdClass();
 618          $record->discussion = $discussion1->id;
 619          $record->parent = $discussion1->firstpost;
 620          $record->userid = $user2->id;
 621          $discussion1reply1 = $forumgenerator->create_post($record);
 622          $filename = 'shouldbeanimage.jpg';
 623          // Add a fake inline image to the post.
 624          $filerecordinline = array(
 625              'contextid' => $forum1context->id,
 626              'component' => 'mod_forum',
 627              'filearea'  => 'post',
 628              'itemid'    => $discussion1reply1->id,
 629              'filepath'  => '/',
 630              'filename'  => $filename,
 631          );
 632          $fs = get_file_storage();
 633          $timepost = time();
 634          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
 635  
 636          $record->parent = $discussion1reply1->id;
 637          $record->userid = $user3->id;
 638          $discussion1reply2 = $forumgenerator->create_post($record);
 639  
 640          // Enrol the user in the  course.
 641          $enrol = enrol_get_plugin('manual');
 642          // Following line enrol and assign default role id to the user.
 643          // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
 644          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 645          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 646  
 647          // Delete one user, to test that we still receive posts by this user.
 648          delete_user($user3);
 649          $exporteduser3 = [
 650              'id' => (int) $user3->id,
 651              'fullname' => get_string('deleteduser', 'mod_forum'),
 652              'groups' => [],
 653              'isdeleted' => true,
 654              'urls' => [
 655                  'profile' => $urlfactory->get_author_profile_url($user3entity, $course1->id)->out(false),
 656                  'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
 657              ]
 658          ];
 659  
 660          // Create what we expect to be returned when querying the discussion.
 661          $expectedposts = array(
 662              'posts' => array(),
 663              'ratinginfo' => array(
 664                  'contextid' => $forum1context->id,
 665                  'component' => 'mod_forum',
 666                  'ratingarea' => 'post',
 667                  'canviewall' => null,
 668                  'canviewany' => null,
 669                  'scales' => array(),
 670                  'ratings' => array(),
 671              ),
 672              'warnings' => array(),
 673          );
 674  
 675          // User pictures are initially empty, we should get the links once the external function is called.
 676          $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion);
 677          $isolatedurl->params(['parent' => $discussion1reply2->id]);
 678          $message = file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
 679              $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id);
 680          $expectedposts['posts'][] = array(
 681              'id' => $discussion1reply2->id,
 682              'discussionid' => $discussion1reply2->discussion,
 683              'parentid' => $discussion1reply2->parent,
 684              'hasparent' => true,
 685              'timecreated' => $discussion1reply2->created,
 686              'subject' => $discussion1reply2->subject,
 687              'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
 688              'message' => $message,
 689              'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 690              'unread' => null,
 691              'isdeleted' => false,
 692              'isprivatereply' => false,
 693              'haswordcount' => true,
 694              'wordcount' => count_words($message),
 695              'charcount' => count_letters($message),
 696              'author'=> $exporteduser3,
 697              'attachments' => [],
 698              'tags' => [],
 699              'html' => [
 700                  'rating' => null,
 701                  'taglist' => null,
 702                  'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser3, $discussion1reply2->created)
 703              ],
 704              'capabilities' => [
 705                  'view' => 1,
 706                  'edit' => 0,
 707                  'delete' => 0,
 708                  'split' => 0,
 709                  'reply' => 1,
 710                  'export' => 0,
 711                  'controlreadstatus' => 0,
 712                  'canreplyprivately' => 0,
 713                  'selfenrol' => 0
 714              ],
 715              'urls' => [
 716                  'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
 717                  'viewisolated' => $isolatedurl->out(false),
 718                  'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->parent),
 719                  'edit' => null,
 720                  'delete' =>null,
 721                  'split' => null,
 722                  'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
 723                      'reply' => $discussion1reply2->id
 724                  ]))->out(false),
 725                  'export' => null,
 726                  'markasread' => null,
 727                  'markasunread' => null,
 728                  'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion),
 729              ],
 730          );
 731  
 732  
 733          $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
 734          $isolatedurl->params(['parent' => $discussion1reply1->id]);
 735          $message = file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
 736              $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id);
 737          $expectedposts['posts'][] = array(
 738              'id' => $discussion1reply1->id,
 739              'discussionid' => $discussion1reply1->discussion,
 740              'parentid' => $discussion1reply1->parent,
 741              'hasparent' => true,
 742              'timecreated' => $discussion1reply1->created,
 743              'subject' => $discussion1reply1->subject,
 744              'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
 745              'message' => $message,
 746              'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 747              'unread' => null,
 748              'isdeleted' => false,
 749              'isprivatereply' => false,
 750              'haswordcount' => true,
 751              'wordcount' => count_words($message),
 752              'charcount' => count_letters($message),
 753              'author'=> $exporteduser2,
 754              'attachments' => [],
 755              'tags' => [],
 756              'html' => [
 757                  'rating' => null,
 758                  'taglist' => null,
 759                  'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser2, $discussion1reply1->created)
 760              ],
 761              'capabilities' => [
 762                  'view' => 1,
 763                  'edit' => 0,
 764                  'delete' => 0,
 765                  'split' => 0,
 766                  'reply' => 1,
 767                  'export' => 0,
 768                  'controlreadstatus' => 0,
 769                  'canreplyprivately' => 0,
 770                  'selfenrol' => 0
 771              ],
 772              'urls' => [
 773                  'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
 774                  'viewisolated' => $isolatedurl->out(false),
 775                  'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->parent),
 776                  'edit' => null,
 777                  'delete' =>null,
 778                  'split' => null,
 779                  'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
 780                      'reply' => $discussion1reply1->id
 781                  ]))->out(false),
 782                  'export' => null,
 783                  'markasread' => null,
 784                  'markasunread' => null,
 785                  'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion),
 786              ],
 787          );
 788  
 789          // Test a discussion with two additional posts (total 3 posts).
 790          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
 791          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 792          $this->assertEquals(3, count($posts['posts']));
 793  
 794          // Unset the initial discussion post.
 795          array_pop($posts['posts']);
 796          $this->assertEquals($expectedposts, $posts);
 797  
 798          // Check we receive the unread count correctly on tracked forum.
 799          forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
 800          $result = mod_forum_external::get_forums_by_courses(array($course1->id));
 801          $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
 802          foreach ($result as $f) {
 803              if ($f['id'] == $forum2->id) {
 804                  $this->assertEquals(1, $f['unreadpostscount']);
 805              }
 806          }
 807  
 808          // Test discussion without additional posts. There should be only one post (the one created by the discussion).
 809          $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC');
 810          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 811          $this->assertEquals(1, count($posts['posts']));
 812  
 813          // Test discussion tracking on not tracked forum.
 814          $result = mod_forum_external::view_forum_discussion($discussion1->id);
 815          $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
 816          $this->assertTrue($result['status']);
 817          $this->assertEmpty($result['warnings']);
 818  
 819          // Test posts have not been marked as read.
 820          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
 821          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 822          foreach ($posts['posts'] as $post) {
 823              $this->assertNull($post['unread']);
 824          }
 825  
 826          // Test discussion tracking on tracked forum.
 827          $result = mod_forum_external::view_forum_discussion($discussion3->id);
 828          $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
 829          $this->assertTrue($result['status']);
 830          $this->assertEmpty($result['warnings']);
 831  
 832          // Test posts have been marked as read.
 833          $posts = mod_forum_external::get_discussion_posts($discussion3->id, 'modified', 'DESC');
 834          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 835          foreach ($posts['posts'] as $post) {
 836              $this->assertFalse($post['unread']);
 837          }
 838  
 839          // Check we receive 0 unread posts.
 840          forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
 841          $result = mod_forum_external::get_forums_by_courses(array($course1->id));
 842          $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
 843          foreach ($result as $f) {
 844              if ($f['id'] == $forum2->id) {
 845                  $this->assertEquals(0, $f['unreadpostscount']);
 846              }
 847          }
 848      }
 849  
 850      /**
 851       * Test get forum posts
 852       */
 853      public function test_mod_forum_get_forum_discussion_posts_deleted() {
 854          global $CFG, $PAGE;
 855  
 856          $this->resetAfterTest(true);
 857          $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
 858  
 859          // Create a course and enrol some users in it.
 860          $course1 = self::getDataGenerator()->create_course();
 861  
 862          // Create users.
 863          $user1 = self::getDataGenerator()->create_user();
 864          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 865          $user2 = self::getDataGenerator()->create_user();
 866          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 867  
 868          // Set the first created user to the test user.
 869          self::setUser($user1);
 870  
 871          // Create test data.
 872          $forum1 = self::getDataGenerator()->create_module('forum', (object) [
 873                  'course' => $course1->id,
 874              ]);
 875          $forum1context = context_module::instance($forum1->cmid);
 876  
 877          // Add discussions to the forum.
 878          $discussion = $generator->create_discussion((object) [
 879                  'course' => $course1->id,
 880                  'userid' => $user1->id,
 881                  'forum' => $forum1->id,
 882              ]);
 883  
 884          $discussion2 = $generator->create_discussion((object) [
 885                  'course' => $course1->id,
 886                  'userid' => $user2->id,
 887                  'forum' => $forum1->id,
 888              ]);
 889  
 890          // Add replies to the discussion.
 891          $discussionreply1 = $generator->create_post((object) [
 892                  'discussion' => $discussion->id,
 893                  'parent' => $discussion->firstpost,
 894                  'userid' => $user2->id,
 895              ]);
 896          $discussionreply2 = $generator->create_post((object) [
 897                  'discussion' => $discussion->id,
 898                  'parent' => $discussionreply1->id,
 899                  'userid' => $user2->id,
 900                  'subject' => '',
 901                  'message' => '',
 902                  'messageformat' => FORMAT_PLAIN,
 903                  'deleted' => 1,
 904              ]);
 905          $discussionreply3 = $generator->create_post((object) [
 906                  'discussion' => $discussion->id,
 907                  'parent' => $discussion->firstpost,
 908                  'userid' => $user2->id,
 909              ]);
 910  
 911          // Test where some posts have been marked as deleted.
 912          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id, 'modified', 'DESC');
 913          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 914          $deletedsubject = get_string('privacy:request:delete:post:subject', 'mod_forum');
 915          $deletedmessage = get_string('privacy:request:delete:post:message', 'mod_forum');
 916  
 917          foreach ($posts['posts'] as $post) {
 918              if ($post['id'] == $discussionreply2->id) {
 919                  $this->assertTrue($post['deleted']);
 920                  $this->assertEquals($deletedsubject, $post['subject']);
 921                  $this->assertEquals($deletedmessage, $post['message']);
 922              } else {
 923                  $this->assertFalse($post['deleted']);
 924                  $this->assertNotEquals($deletedsubject, $post['subject']);
 925                  $this->assertNotEquals($deletedmessage, $post['message']);
 926              }
 927          }
 928      }
 929  
 930      /**
 931       * Test get forum posts (qanda forum)
 932       */
 933      public function test_mod_forum_get_forum_discussion_posts_qanda() {
 934          global $CFG, $DB;
 935  
 936          $this->resetAfterTest(true);
 937  
 938          $record = new stdClass();
 939          $user1 = self::getDataGenerator()->create_user($record);
 940          $user2 = self::getDataGenerator()->create_user();
 941  
 942          // Set the first created user to the test user.
 943          self::setUser($user1);
 944  
 945          // Create course to add the module.
 946          $course1 = self::getDataGenerator()->create_course();
 947          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 948          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 949  
 950          // Forum with tracking off.
 951          $record = new stdClass();
 952          $record->course = $course1->id;
 953          $record->type = 'qanda';
 954          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 955          $forum1context = context_module::instance($forum1->cmid);
 956  
 957          // Add discussions to the forums.
 958          $record = new stdClass();
 959          $record->course = $course1->id;
 960          $record->userid = $user2->id;
 961          $record->forum = $forum1->id;
 962          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 963  
 964          // Add 1 reply (not the actual user).
 965          $record = new stdClass();
 966          $record->discussion = $discussion1->id;
 967          $record->parent = $discussion1->firstpost;
 968          $record->userid = $user2->id;
 969          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 970  
 971          // We still see only the original post.
 972          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
 973          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 974          $this->assertEquals(1, count($posts['posts']));
 975  
 976          // Add a new reply, the user is going to be able to see only the original post and their new post.
 977          $record = new stdClass();
 978          $record->discussion = $discussion1->id;
 979          $record->parent = $discussion1->firstpost;
 980          $record->userid = $user1->id;
 981          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 982  
 983          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
 984          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 985          $this->assertEquals(2, count($posts['posts']));
 986  
 987          // Now, we can fake the time of the user post, so he can se the rest of the discussion posts.
 988          $discussion1reply2->created -= $CFG->maxeditingtime * 2;
 989          $DB->update_record('forum_posts', $discussion1reply2);
 990  
 991          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
 992          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
 993          $this->assertEquals(3, count($posts['posts']));
 994      }
 995  
 996      /**
 997       * Test get forum discussions paginated
 998       */
 999      public function test_mod_forum_get_forum_discussions_paginated() {
1000          global $USER, $CFG, $DB, $PAGE;
1001  
1002          $this->resetAfterTest(true);
1003  
1004          // Set the CFG variable to allow track forums.
1005          $CFG->forum_trackreadposts = true;
1006  
1007          // Create a user who can track forums.
1008          $record = new stdClass();
1009          $record->trackforums = true;
1010          $user1 = self::getDataGenerator()->create_user($record);
1011          // Create a bunch of other users to post.
1012          $user2 = self::getDataGenerator()->create_user();
1013          $user3 = self::getDataGenerator()->create_user();
1014          $user4 = self::getDataGenerator()->create_user();
1015  
1016          // Set the first created user to the test user.
1017          self::setUser($user1);
1018  
1019          // Create courses to add the modules.
1020          $course1 = self::getDataGenerator()->create_course();
1021  
1022          // First forum with tracking off.
1023          $record = new stdClass();
1024          $record->course = $course1->id;
1025          $record->trackingtype = FORUM_TRACKING_OFF;
1026          $forum1 = self::getDataGenerator()->create_module('forum', $record);
1027  
1028          // Add discussions to the forums.
1029          $record = new stdClass();
1030          $record->course = $course1->id;
1031          $record->userid = $user1->id;
1032          $record->forum = $forum1->id;
1033          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1034  
1035          // Add three replies to the discussion 1 from different users.
1036          $record = new stdClass();
1037          $record->discussion = $discussion1->id;
1038          $record->parent = $discussion1->firstpost;
1039          $record->userid = $user2->id;
1040          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1041  
1042          $record->parent = $discussion1reply1->id;
1043          $record->userid = $user3->id;
1044          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1045  
1046          $record->userid = $user4->id;
1047          $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1048  
1049          // Enrol the user in the first course.
1050          $enrol = enrol_get_plugin('manual');
1051  
1052          // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1053          $enrolinstances = enrol_get_instances($course1->id, true);
1054          foreach ($enrolinstances as $courseenrolinstance) {
1055              if ($courseenrolinstance->enrol == "manual") {
1056                  $instance1 = $courseenrolinstance;
1057                  break;
1058              }
1059          }
1060          $enrol->enrol_user($instance1, $user1->id);
1061  
1062          // Delete one user.
1063          delete_user($user4);
1064  
1065          // Assign capabilities to view discussions for forum 1.
1066          $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1067          $context = context_module::instance($cm->id);
1068          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1069          $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1070  
1071          // Create what we expect to be returned when querying the forums.
1072  
1073          $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
1074  
1075          // User pictures are initially empty, we should get the links once the external function is called.
1076          $expecteddiscussions = array(
1077                  'id' => $discussion1->firstpost,
1078                  'name' => $discussion1->name,
1079                  'groupid' => (int) $discussion1->groupid,
1080                  'timemodified' => $discussion1reply3->created,
1081                  'usermodified' => (int) $discussion1reply3->userid,
1082                  'timestart' => (int) $discussion1->timestart,
1083                  'timeend' => (int) $discussion1->timeend,
1084                  'discussion' => $discussion1->id,
1085                  'parent' => 0,
1086                  'userid' => (int) $discussion1->userid,
1087                  'created' => (int) $post1->created,
1088                  'modified' => (int) $post1->modified,
1089                  'mailed' => (int) $post1->mailed,
1090                  'subject' => $post1->subject,
1091                  'message' => $post1->message,
1092                  'messageformat' => (int) $post1->messageformat,
1093                  'messagetrust' => (int) $post1->messagetrust,
1094                  'attachment' => $post1->attachment,
1095                  'totalscore' => (int) $post1->totalscore,
1096                  'mailnow' => (int) $post1->mailnow,
1097                  'userfullname' => fullname($user1),
1098                  'usermodifiedfullname' => fullname($user4),
1099                  'userpictureurl' => '',
1100                  'usermodifiedpictureurl' => '',
1101                  'numreplies' => 3,
1102                  'numunread' => 0,
1103                  'pinned' => (bool) FORUM_DISCUSSION_UNPINNED,
1104                  'locked' => false,
1105                  'canreply' => false,
1106                  'canlock' => false
1107              );
1108  
1109          // Call the external function passing forum id.
1110          $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
1111          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1112          $expectedreturn = array(
1113              'discussions' => array($expecteddiscussions),
1114              'warnings' => array()
1115          );
1116  
1117          // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
1118          $userpicture = new user_picture($user1);
1119          $userpicture->size = 1; // Size f1.
1120          $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1121  
1122          $userpicture = new user_picture($user4);
1123          $userpicture->size = 1; // Size f1.
1124          $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1125  
1126          $this->assertEquals($expectedreturn, $discussions);
1127  
1128          // Call without required view discussion capability.
1129          $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1130          try {
1131              mod_forum_external::get_forum_discussions_paginated($forum1->id);
1132              $this->fail('Exception expected due to missing capability.');
1133          } catch (moodle_exception $e) {
1134              $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1135          }
1136  
1137          // Unenrol user from second course.
1138          $enrol->unenrol_user($instance1, $user1->id);
1139  
1140          // Call for the second course we unenrolled the user from, make sure exception thrown.
1141          try {
1142              mod_forum_external::get_forum_discussions_paginated($forum1->id);
1143              $this->fail('Exception expected due to being unenrolled from the course.');
1144          } catch (moodle_exception $e) {
1145              $this->assertEquals('requireloginerror', $e->errorcode);
1146          }
1147  
1148          $this->setAdminUser();
1149          $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
1150          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1151          $this->assertTrue($discussions['discussions'][0]['canlock']);
1152      }
1153  
1154      /**
1155       * Test get forum discussions paginated (qanda forums)
1156       */
1157      public function test_mod_forum_get_forum_discussions_paginated_qanda() {
1158  
1159          $this->resetAfterTest(true);
1160  
1161          // Create courses to add the modules.
1162          $course = self::getDataGenerator()->create_course();
1163  
1164          $user1 = self::getDataGenerator()->create_user();
1165          $user2 = self::getDataGenerator()->create_user();
1166  
1167          // First forum with tracking off.
1168          $record = new stdClass();
1169          $record->course = $course->id;
1170          $record->type = 'qanda';
1171          $forum = self::getDataGenerator()->create_module('forum', $record);
1172  
1173          // Add discussions to the forums.
1174          $discussionrecord = new stdClass();
1175          $discussionrecord->course = $course->id;
1176          $discussionrecord->userid = $user2->id;
1177          $discussionrecord->forum = $forum->id;
1178          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
1179  
1180          self::setAdminUser();
1181          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1182          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1183  
1184          $this->assertCount(1, $discussions['discussions']);
1185          $this->assertCount(0, $discussions['warnings']);
1186  
1187          self::setUser($user1);
1188          $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1189  
1190          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1191          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1192  
1193          $this->assertCount(1, $discussions['discussions']);
1194          $this->assertCount(0, $discussions['warnings']);
1195  
1196      }
1197  
1198      /**
1199       * Test get forum discussions
1200       */
1201      public function test_mod_forum_get_forum_discussions() {
1202          global $CFG, $DB, $PAGE;
1203  
1204          $this->resetAfterTest(true);
1205  
1206          // Set the CFG variable to allow track forums.
1207          $CFG->forum_trackreadposts = true;
1208  
1209          // Create a user who can track forums.
1210          $record = new stdClass();
1211          $record->trackforums = true;
1212          $user1 = self::getDataGenerator()->create_user($record);
1213          // Create a bunch of other users to post.
1214          $user2 = self::getDataGenerator()->create_user();
1215          $user3 = self::getDataGenerator()->create_user();
1216          $user4 = self::getDataGenerator()->create_user();
1217  
1218          // Set the first created user to the test user.
1219          self::setUser($user1);
1220  
1221          // Create courses to add the modules.
1222          $course1 = self::getDataGenerator()->create_course();
1223  
1224          // First forum with tracking off.
1225          $record = new stdClass();
1226          $record->course = $course1->id;
1227          $record->trackingtype = FORUM_TRACKING_OFF;
1228          $forum1 = self::getDataGenerator()->create_module('forum', $record);
1229  
1230          // Add discussions to the forums.
1231          $record = new stdClass();
1232          $record->course = $course1->id;
1233          $record->userid = $user1->id;
1234          $record->forum = $forum1->id;
1235          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1236  
1237          // Add three replies to the discussion 1 from different users.
1238          $record = new stdClass();
1239          $record->discussion = $discussion1->id;
1240          $record->parent = $discussion1->firstpost;
1241          $record->userid = $user2->id;
1242          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1243  
1244          $record->parent = $discussion1reply1->id;
1245          $record->userid = $user3->id;
1246          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1247  
1248          $record->userid = $user4->id;
1249          $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1250  
1251          // Enrol the user in the first course.
1252          $enrol = enrol_get_plugin('manual');
1253  
1254          // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1255          $enrolinstances = enrol_get_instances($course1->id, true);
1256          foreach ($enrolinstances as $courseenrolinstance) {
1257              if ($courseenrolinstance->enrol == "manual") {
1258                  $instance1 = $courseenrolinstance;
1259                  break;
1260              }
1261          }
1262          $enrol->enrol_user($instance1, $user1->id);
1263  
1264          // Delete one user.
1265          delete_user($user4);
1266  
1267          // Assign capabilities to view discussions for forum 1.
1268          $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1269          $context = context_module::instance($cm->id);
1270          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1271          $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1272  
1273          // Create what we expect to be returned when querying the forums.
1274  
1275          $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
1276  
1277          // User pictures are initially empty, we should get the links once the external function is called.
1278          $expecteddiscussions = array(
1279              'id' => $discussion1->firstpost,
1280              'name' => $discussion1->name,
1281              'groupid' => (int) $discussion1->groupid,
1282              'timemodified' => (int) $discussion1reply3->created,
1283              'usermodified' => (int) $discussion1reply3->userid,
1284              'timestart' => (int) $discussion1->timestart,
1285              'timeend' => (int) $discussion1->timeend,
1286              'discussion' => (int) $discussion1->id,
1287              'parent' => 0,
1288              'userid' => (int) $discussion1->userid,
1289              'created' => (int) $post1->created,
1290              'modified' => (int) $post1->modified,
1291              'mailed' => (int) $post1->mailed,
1292              'subject' => $post1->subject,
1293              'message' => $post1->message,
1294              'messageformat' => (int) $post1->messageformat,
1295              'messagetrust' => (int) $post1->messagetrust,
1296              'attachment' => $post1->attachment,
1297              'totalscore' => (int) $post1->totalscore,
1298              'mailnow' => (int) $post1->mailnow,
1299              'userfullname' => fullname($user1),
1300              'usermodifiedfullname' => fullname($user4),
1301              'userpictureurl' => '',
1302              'usermodifiedpictureurl' => '',
1303              'numreplies' => 3,
1304              'numunread' => 0,
1305              'pinned' => (bool) FORUM_DISCUSSION_UNPINNED,
1306              'locked' => false,
1307              'canreply' => false,
1308              'canlock' => false,
1309              'starred' => false,
1310              'canfavourite' => true
1311          );
1312  
1313          // Call the external function passing forum id.
1314          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1315          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1316          $expectedreturn = array(
1317              'discussions' => array($expecteddiscussions),
1318              'warnings' => array()
1319          );
1320  
1321          // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
1322          $userpicture = new user_picture($user1);
1323          $userpicture->size = 2; // Size f2.
1324          $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1325  
1326          $userpicture = new user_picture($user4);
1327          $userpicture->size = 2; // Size f2.
1328          $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1329  
1330          $this->assertEquals($expectedreturn, $discussions);
1331  
1332          // Test the starring functionality return.
1333          $t = mod_forum_external::toggle_favourite_state($discussion1->id, 1);
1334          $expectedreturn['discussions'][0]['starred'] = true;
1335          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1336          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1337          $this->assertEquals($expectedreturn, $discussions);
1338  
1339          // Call without required view discussion capability.
1340          $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1341          try {
1342              mod_forum_external::get_forum_discussions($forum1->id);
1343              $this->fail('Exception expected due to missing capability.');
1344          } catch (moodle_exception $e) {
1345              $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1346          }
1347  
1348          // Unenrol user from second course.
1349          $enrol->unenrol_user($instance1, $user1->id);
1350  
1351          // Call for the second course we unenrolled the user from, make sure exception thrown.
1352          try {
1353              mod_forum_external::get_forum_discussions($forum1->id);
1354              $this->fail('Exception expected due to being unenrolled from the course.');
1355          } catch (moodle_exception $e) {
1356              $this->assertEquals('requireloginerror', $e->errorcode);
1357          }
1358  
1359          $this->setAdminUser();
1360          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1361          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1362          $this->assertTrue($discussions['discussions'][0]['canlock']);
1363      }
1364  
1365      /**
1366       * Test the sorting in get forum discussions
1367       */
1368      public function test_mod_forum_get_forum_discussions_sorting() {
1369          global $CFG, $DB, $PAGE;
1370  
1371          $this->resetAfterTest(true);
1372  
1373          // Set the CFG variable to allow track forums.
1374          $CFG->forum_trackreadposts = true;
1375  
1376          // Create a user who can track forums.
1377          $record = new stdClass();
1378          $record->trackforums = true;
1379          $user1 = self::getDataGenerator()->create_user($record);
1380          // Create a bunch of other users to post.
1381          $user2 = self::getDataGenerator()->create_user();
1382          $user3 = self::getDataGenerator()->create_user();
1383          $user4 = self::getDataGenerator()->create_user();
1384  
1385          // Set the first created user to the test user.
1386          self::setUser($user1);
1387  
1388          // Create courses to add the modules.
1389          $course1 = self::getDataGenerator()->create_course();
1390  
1391          // Enrol the user in the first course.
1392          $enrol = enrol_get_plugin('manual');
1393  
1394          // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1395          $enrolinstances = enrol_get_instances($course1->id, true);
1396          foreach ($enrolinstances as $courseenrolinstance) {
1397              if ($courseenrolinstance->enrol == "manual") {
1398                  $instance1 = $courseenrolinstance;
1399                  break;
1400              }
1401          }
1402          $enrol->enrol_user($instance1, $user1->id);
1403  
1404          // First forum with tracking off.
1405          $record = new stdClass();
1406          $record->course = $course1->id;
1407          $record->trackingtype = FORUM_TRACKING_OFF;
1408          $forum1 = self::getDataGenerator()->create_module('forum', $record);
1409  
1410          // Assign capabilities to view discussions for forum 1.
1411          $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1412          $context = context_module::instance($cm->id);
1413          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1414          $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1415  
1416          // Add discussions to the forums.
1417          $record = new stdClass();
1418          $record->course = $course1->id;
1419          $record->userid = $user1->id;
1420          $record->forum = $forum1->id;
1421          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1422          sleep(1);
1423  
1424          // Add three replies to the discussion 1 from different users.
1425          $record = new stdClass();
1426          $record->discussion = $discussion1->id;
1427          $record->parent = $discussion1->firstpost;
1428          $record->userid = $user2->id;
1429          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1430          sleep(1);
1431  
1432          $record->parent = $discussion1reply1->id;
1433          $record->userid = $user3->id;
1434          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1435          sleep(1);
1436  
1437          $record->userid = $user4->id;
1438          $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1439          sleep(1);
1440  
1441          // Create discussion2.
1442          $record2 = new stdClass();
1443          $record2->course = $course1->id;
1444          $record2->userid = $user1->id;
1445          $record2->forum = $forum1->id;
1446          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record2);
1447          sleep(1);
1448  
1449          // Add one reply to the discussion 2.
1450          $record2 = new stdClass();
1451          $record2->discussion = $discussion2->id;
1452          $record2->parent = $discussion2->firstpost;
1453          $record2->userid = $user2->id;
1454          $discussion2reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record2);
1455          sleep(1);
1456  
1457          // Create discussion 3.
1458          $record3 = new stdClass();
1459          $record3->course = $course1->id;
1460          $record3->userid = $user1->id;
1461          $record3->forum = $forum1->id;
1462          $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record3);
1463          sleep(1);
1464  
1465          // Add two replies to the discussion 3.
1466          $record3 = new stdClass();
1467          $record3->discussion = $discussion3->id;
1468          $record3->parent = $discussion3->firstpost;
1469          $record3->userid = $user2->id;
1470          $discussion3reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3);
1471          sleep(1);
1472  
1473          $record3->parent = $discussion3reply1->id;
1474          $record3->userid = $user3->id;
1475          $discussion3reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3);
1476  
1477          // Call the external function passing forum id.
1478          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1479          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1480          // Discussions should be ordered by last post date in descending order by default.
1481          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id);
1482          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1483          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1484  
1485          $vaultfactory = \mod_forum\local\container::get_vault_factory();
1486          $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault();
1487  
1488          // Call the external function passing forum id and sort order parameter.
1489          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC);
1490          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1491          // Discussions should be ordered by last post date in ascending order.
1492          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1493          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1494          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1495  
1496          // Call the external function passing forum id and sort order parameter.
1497          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_DESC);
1498          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1499          // Discussions should be ordered by discussion creation date in descending order.
1500          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id);
1501          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1502          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1503  
1504          // Call the external function passing forum id and sort order parameter.
1505          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_ASC);
1506          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1507          // Discussions should be ordered by discussion creation date in ascending order.
1508          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1509          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1510          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1511  
1512          // Call the external function passing forum id and sort order parameter.
1513          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_DESC);
1514          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1515          // Discussions should be ordered by the number of replies in descending order.
1516          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1517          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1518          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion2->id);
1519  
1520          // Call the external function passing forum id and sort order parameter.
1521          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_ASC);
1522          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1523          // Discussions should be ordered by the number of replies in ascending order.
1524          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1525          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1526          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1527  
1528          // Pin discussion2.
1529          $DB->update_record('forum_discussions',
1530              (object) array('id' => $discussion2->id, 'pinned' => FORUM_DISCUSSION_PINNED));
1531  
1532          // Call the external function passing forum id.
1533          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1534          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1535          // Discussions should be ordered by last post date in descending order by default.
1536          // Pinned discussions should be at the top of the list.
1537          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1538          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1539          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1540  
1541          // Call the external function passing forum id and sort order parameter.
1542          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC);
1543          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1544          // Discussions should be ordered by last post date in ascending order.
1545          // Pinned discussions should be at the top of the list.
1546          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1547          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion1->id);
1548          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1549      }
1550  
1551      /**
1552       * Test add_discussion_post
1553       */
1554      public function test_add_discussion_post() {
1555          global $CFG;
1556  
1557          $this->resetAfterTest(true);
1558  
1559          $user = self::getDataGenerator()->create_user();
1560          $otheruser = self::getDataGenerator()->create_user();
1561  
1562          self::setAdminUser();
1563  
1564          // Create course to add the module.
1565          $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1566  
1567          // Forum with tracking off.
1568          $record = new stdClass();
1569          $record->course = $course->id;
1570          $forum = self::getDataGenerator()->create_module('forum', $record);
1571          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1572          $forumcontext = context_module::instance($forum->cmid);
1573  
1574          // Add discussions to the forums.
1575          $record = new stdClass();
1576          $record->course = $course->id;
1577          $record->userid = $user->id;
1578          $record->forum = $forum->id;
1579          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1580  
1581          // Try to post (user not enrolled).
1582          self::setUser($user);
1583          try {
1584              mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1585              $this->fail('Exception expected due to being unenrolled from the course.');
1586          } catch (moodle_exception $e) {
1587              $this->assertEquals('requireloginerror', $e->errorcode);
1588          }
1589  
1590          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1591          $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
1592  
1593          $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1594          $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1595  
1596          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1597          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1598          // We receive the discussion and the post.
1599          $this->assertEquals(2, count($posts['posts']));
1600  
1601          $tested = false;
1602          foreach ($posts['posts'] as $thispost) {
1603              if ($createdpost['postid'] == $thispost['id']) {
1604                  $this->assertEquals('some subject', $thispost['subject']);
1605                  $this->assertEquals('some text here...', $thispost['message']);
1606                  $this->assertEquals(FORMAT_HTML, $thispost['messageformat']); // This is the default if format was not specified.
1607                  $tested = true;
1608              }
1609          }
1610          $this->assertTrue($tested);
1611  
1612          // Let's simulate a call with any other format, it should be stored that way.
1613          global $DB; // Yes, we are going to use DB facilities too, because cannot rely on other functions for checking
1614                      // the format. They eat it completely (going back to FORMAT_HTML. So we only can trust DB for further
1615                      // processing.
1616          $formats = [FORMAT_PLAIN, FORMAT_MOODLE, FORMAT_MARKDOWN, FORMAT_HTML];
1617          $options = [];
1618          foreach ($formats as $format) {
1619              $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost,
1620                  'with some format', 'some formatted here...', $options, $format);
1621              $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1622              $dbformat = $DB->get_field('forum_posts', 'messageformat', ['id' => $createdpost['postid']]);
1623              $this->assertEquals($format, $dbformat);
1624          }
1625  
1626          // Now let's try the 'topreferredformat' option. That should end with the content
1627          // transformed and the format being FORMAT_HTML (when, like in this case,  user preferred
1628          // format is HTML, inferred from editor in preferences).
1629          $options = [['name' => 'topreferredformat', 'value' => true]];
1630          $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost,
1631              'interesting subject', 'with some https://example.com link', $options, FORMAT_MOODLE);
1632          $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1633          $dbpost = $DB->get_record('forum_posts', ['id' => $createdpost['postid']]);
1634          // Format HTML and content converted, we should get.
1635          $this->assertEquals(FORMAT_HTML, $dbpost->messageformat);
1636          $this->assertEquals('<div class="text_to_html">with some https://example.com link</div>', $dbpost->message);
1637  
1638          // Test inline and regular attachment in post
1639          // Create a file in a draft area for inline attachments.
1640          $draftidinlineattach = file_get_unused_draft_itemid();
1641          $draftidattach = file_get_unused_draft_itemid();
1642          self::setUser($user);
1643          $usercontext = context_user::instance($user->id);
1644          $filepath = '/';
1645          $filearea = 'draft';
1646          $component = 'user';
1647          $filenameimg = 'shouldbeanimage.txt';
1648          $filerecordinline = array(
1649              'contextid' => $usercontext->id,
1650              'component' => $component,
1651              'filearea'  => $filearea,
1652              'itemid'    => $draftidinlineattach,
1653              'filepath'  => $filepath,
1654              'filename'  => $filenameimg,
1655          );
1656          $fs = get_file_storage();
1657  
1658          // Create a file in a draft area for regular attachments.
1659          $filerecordattach = $filerecordinline;
1660          $attachfilename = 'attachment.txt';
1661          $filerecordattach['filename'] = $attachfilename;
1662          $filerecordattach['itemid'] = $draftidattach;
1663          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
1664          $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1665  
1666          $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1667                           array('name' => 'attachmentsid', 'value' => $draftidattach));
1668          $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot
1669                       . "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}"
1670                       . '" alt="inlineimage">.';
1671          $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment',
1672                                                                 $dummytext, $options);
1673          $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1674  
1675          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1676          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1677          // We receive the discussion and the post.
1678          // Can't guarantee order of posts during tests.
1679          $postfound = false;
1680          foreach ($posts['posts'] as $thispost) {
1681              if ($createdpost['postid'] == $thispost['id']) {
1682                  $this->assertEquals($createdpost['postid'], $thispost['id']);
1683                  $this->assertEquals($thispost['attachment'], 1, "There should be a non-inline attachment");
1684                  $this->assertCount(1, $thispost['attachments'], "There should be 1 attachment");
1685                  $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1686                  $this->assertContains('pluginfile.php', $thispost['message']);
1687                  $postfound = true;
1688                  break;
1689              }
1690          }
1691  
1692          $this->assertTrue($postfound);
1693  
1694          // Check not posting in groups the user is not member of.
1695          $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1696          groups_add_member($group->id, $otheruser->id);
1697  
1698          $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1699          $record->forum = $forum->id;
1700          $record->userid = $otheruser->id;
1701          $record->groupid = $group->id;
1702          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1703  
1704          try {
1705              mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1706              $this->fail('Exception expected due to invalid permissions for posting.');
1707          } catch (moodle_exception $e) {
1708              $this->assertEquals('nopostforum', $e->errorcode);
1709          }
1710      }
1711  
1712      /**
1713       * Test add_discussion_post and auto subscription to a discussion.
1714       */
1715      public function test_add_discussion_post_subscribe_discussion() {
1716          global $USER;
1717  
1718          $this->resetAfterTest(true);
1719  
1720          self::setAdminUser();
1721  
1722          $user = self::getDataGenerator()->create_user();
1723          $admin = get_admin();
1724          // Create course to add the module.
1725          $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1726  
1727          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1728  
1729          // Forum with tracking off.
1730          $record = new stdClass();
1731          $record->course = $course->id;
1732          $forum = self::getDataGenerator()->create_module('forum', $record);
1733          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1734  
1735          // Add discussions to the forums.
1736          $record = new stdClass();
1737          $record->course = $course->id;
1738          $record->userid = $admin->id;
1739          $record->forum = $forum->id;
1740          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1741          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1742  
1743          // Try to post as user.
1744          self::setUser($user);
1745          // Enable auto subscribe discussion.
1746          $USER->autosubscribe = true;
1747          // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference enabled).
1748          mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject', 'some text here...');
1749  
1750          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
1751          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1752          // We receive the discussion and the post.
1753          $this->assertEquals(2, count($posts['posts']));
1754          // The user should be subscribed to the discussion after adding a discussion post.
1755          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1756  
1757          // Disable auto subscribe discussion.
1758          $USER->autosubscribe = false;
1759          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1760          // Add a discussion post in a forum discussion where the user is subscribed (auto-subscribe preference disabled).
1761          mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject 1', 'some text here 1...');
1762  
1763          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
1764          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1765          // We receive the discussion and the post.
1766          $this->assertEquals(3, count($posts['posts']));
1767          // The user should still be subscribed to the discussion after adding a discussion post.
1768          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1769  
1770          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1771          // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled).
1772          mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...');
1773  
1774          $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
1775          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1776          // We receive the discussion and the post.
1777          $this->assertEquals(2, count($posts['posts']));
1778          // The user should still not be subscribed to the discussion after adding a discussion post.
1779          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1780  
1781          // Passing a value for the discussionsubscribe option parameter.
1782          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1783          // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled),
1784          // and the option parameter 'discussionsubscribe' => true in the webservice.
1785          $option = array('name' => 'discussionsubscribe', 'value' => true);
1786          $options[] = $option;
1787          mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...',
1788              $options);
1789  
1790          $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
1791          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1792          // We receive the discussion and the post.
1793          $this->assertEquals(3, count($posts['posts']));
1794          // The user should now be subscribed to the discussion after adding a discussion post.
1795          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1796      }
1797  
1798      /*
1799       * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
1800       */
1801      public function test_add_discussion() {
1802          global $CFG, $USER;
1803          $this->resetAfterTest(true);
1804  
1805          // Create courses to add the modules.
1806          $course = self::getDataGenerator()->create_course();
1807  
1808          $user1 = self::getDataGenerator()->create_user();
1809          $user2 = self::getDataGenerator()->create_user();
1810  
1811          // First forum with tracking off.
1812          $record = new stdClass();
1813          $record->course = $course->id;
1814          $record->type = 'news';
1815          $forum = self::getDataGenerator()->create_module('forum', $record);
1816  
1817          self::setUser($user1);
1818          $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1819  
1820          try {
1821              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1822              $this->fail('Exception expected due to invalid permissions.');
1823          } catch (moodle_exception $e) {
1824              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1825          }
1826  
1827          self::setAdminUser();
1828          $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1829          $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1830  
1831          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1832          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1833  
1834          $this->assertCount(1, $discussions['discussions']);
1835          $this->assertCount(0, $discussions['warnings']);
1836  
1837          $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']);
1838          $this->assertEquals(-1, $discussions['discussions'][0]['groupid']);
1839          $this->assertEquals('the subject', $discussions['discussions'][0]['subject']);
1840          $this->assertEquals('some text here...', $discussions['discussions'][0]['message']);
1841  
1842          $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1,
1843                                                                  array('options' => array('name' => 'discussionpinned',
1844                                                                                           'value' => true)));
1845          $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...');
1846          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1847          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1848          $this->assertCount(3, $discussions['discussions']);
1849          $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']);
1850  
1851          // Test inline and regular attachment in new discussion
1852          // Create a file in a draft area for inline attachments.
1853  
1854          $fs = get_file_storage();
1855  
1856          $draftidinlineattach = file_get_unused_draft_itemid();
1857          $draftidattach = file_get_unused_draft_itemid();
1858  
1859          $usercontext = context_user::instance($USER->id);
1860          $filepath = '/';
1861          $filearea = 'draft';
1862          $component = 'user';
1863          $filenameimg = 'shouldbeanimage.txt';
1864          $filerecord = array(
1865              'contextid' => $usercontext->id,
1866              'component' => $component,
1867              'filearea'  => $filearea,
1868              'itemid'    => $draftidinlineattach,
1869              'filepath'  => $filepath,
1870              'filename'  => $filenameimg,
1871          );
1872  
1873          // Create a file in a draft area for regular attachments.
1874          $filerecordattach = $filerecord;
1875          $attachfilename = 'attachment.txt';
1876          $filerecordattach['filename'] = $attachfilename;
1877          $filerecordattach['itemid'] = $draftidattach;
1878          $fs->create_file_from_string($filerecord, 'image contents (not really)');
1879          $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1880  
1881          $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
1882                      "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
1883                      '" alt="inlineimage">.';
1884  
1885          $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1886                           array('name' => 'attachmentsid', 'value' => $draftidattach));
1887          $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject',
1888                                                                  $dummytext, -1, $options);
1889          $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1890  
1891          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1892          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1893  
1894          $this->assertCount(4, $discussions['discussions']);
1895          $this->assertCount(0, $createddiscussion['warnings']);
1896          // Can't guarantee order of posts during tests.
1897          $postfound = false;
1898          foreach ($discussions['discussions'] as $thisdiscussion) {
1899              if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) {
1900                  $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment");
1901                  $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment");
1902                  $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1903                  $this->assertNotContains('draftfile.php', $thisdiscussion['message']);
1904                  $this->assertContains('pluginfile.php', $thisdiscussion['message']);
1905                  $postfound = true;
1906                  break;
1907              }
1908          }
1909  
1910          $this->assertTrue($postfound);
1911      }
1912  
1913      /**
1914       * Test adding discussions in a course with gorups
1915       */
1916      public function test_add_discussion_in_course_with_groups() {
1917          global $CFG;
1918  
1919          $this->resetAfterTest(true);
1920  
1921          // Create course to add the module.
1922          $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1923          $user = self::getDataGenerator()->create_user();
1924          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1925  
1926          // Forum forcing separate gropus.
1927          $record = new stdClass();
1928          $record->course = $course->id;
1929          $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1930  
1931          // Try to post (user not enrolled).
1932          self::setUser($user);
1933  
1934          // The user is not enroled in any group, try to post in a forum with separate groups.
1935          try {
1936              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1937              $this->fail('Exception expected due to invalid group permissions.');
1938          } catch (moodle_exception $e) {
1939              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1940          }
1941  
1942          try {
1943              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0);
1944              $this->fail('Exception expected due to invalid group permissions.');
1945          } catch (moodle_exception $e) {
1946              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1947          }
1948  
1949          // Create a group.
1950          $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1951  
1952          // Try to post in a group the user is not enrolled.
1953          try {
1954              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1955              $this->fail('Exception expected due to invalid group permissions.');
1956          } catch (moodle_exception $e) {
1957              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1958          }
1959  
1960          // Add the user to a group.
1961          groups_add_member($group->id, $user->id);
1962  
1963          // Try to post in a group the user is not enrolled.
1964          try {
1965              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1);
1966              $this->fail('Exception expected due to invalid group.');
1967          } catch (moodle_exception $e) {
1968              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1969          }
1970  
1971          // Nost add the discussion using a valid group.
1972          $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1973          $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1974  
1975          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1976          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1977  
1978          $this->assertCount(1, $discussions['discussions']);
1979          $this->assertCount(0, $discussions['warnings']);
1980          $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']);
1981          $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1982  
1983          // Now add a discussions without indicating a group. The function should guess the correct group.
1984          $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1985          $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1986  
1987          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1988          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1989  
1990          $this->assertCount(2, $discussions['discussions']);
1991          $this->assertCount(0, $discussions['warnings']);
1992          $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1993          $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1994  
1995          // Enrol the same user in other group.
1996          $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1997          groups_add_member($group2->id, $user->id);
1998  
1999          // Now add a discussions without indicating a group. The function should guess the correct group (the first one).
2000          $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
2001          $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
2002  
2003          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
2004          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
2005  
2006          $this->assertCount(3, $discussions['discussions']);
2007          $this->assertCount(0, $discussions['warnings']);
2008          $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
2009          $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
2010          $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']);
2011  
2012      }
2013  
2014      /*
2015       * Test set_lock_state.
2016       */
2017      public function test_set_lock_state() {
2018          global $DB;
2019          $this->resetAfterTest(true);
2020  
2021          // Create courses to add the modules.
2022          $course = self::getDataGenerator()->create_course();
2023          $user = self::getDataGenerator()->create_user();
2024          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2025  
2026          // First forum with tracking off.
2027          $record = new stdClass();
2028          $record->course = $course->id;
2029          $record->type = 'news';
2030          $forum = self::getDataGenerator()->create_module('forum', $record);
2031  
2032          $record = new stdClass();
2033          $record->course = $course->id;
2034          $record->userid = $user->id;
2035          $record->forum = $forum->id;
2036          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2037  
2038          // User who is a student.
2039          self::setUser($user);
2040          $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
2041  
2042          // Only a teacher should be able to lock a discussion.
2043          try {
2044              $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0);
2045              $this->fail('Exception expected due to missing capability.');
2046          } catch (moodle_exception $e) {
2047              $this->assertEquals('errorcannotlock', $e->errorcode);
2048          }
2049  
2050          // Set the lock.
2051          self::setAdminUser();
2052          $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0);
2053          $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result);
2054          $this->assertTrue($result['locked']);
2055          $this->assertNotEquals(0, $result['times']['locked']);
2056  
2057          // Unset the lock.
2058          $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, time());
2059          $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result);
2060          $this->assertFalse($result['locked']);
2061          $this->assertEquals('0', $result['times']['locked']);
2062      }
2063  
2064      /*
2065       * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests.
2066       */
2067      public function test_can_add_discussion() {
2068          global $DB;
2069          $this->resetAfterTest(true);
2070  
2071          // Create courses to add the modules.
2072          $course = self::getDataGenerator()->create_course();
2073  
2074          $user = self::getDataGenerator()->create_user();
2075  
2076          // First forum with tracking off.
2077          $record = new stdClass();
2078          $record->course = $course->id;
2079          $record->type = 'news';
2080          $forum = self::getDataGenerator()->create_module('forum', $record);
2081  
2082          // User with no permissions to add in a news forum.
2083          self::setUser($user);
2084          $this->getDataGenerator()->enrol_user($user->id, $course->id);
2085  
2086          $result = mod_forum_external::can_add_discussion($forum->id);
2087          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2088          $this->assertFalse($result['status']);
2089          $this->assertFalse($result['canpindiscussions']);
2090          $this->assertTrue($result['cancreateattachment']);
2091  
2092          // Disable attachments.
2093          $DB->set_field('forum', 'maxattachments', 0, array('id' => $forum->id));
2094          $result = mod_forum_external::can_add_discussion($forum->id);
2095          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2096          $this->assertFalse($result['status']);
2097          $this->assertFalse($result['canpindiscussions']);
2098          $this->assertFalse($result['cancreateattachment']);
2099          $DB->set_field('forum', 'maxattachments', 1, array('id' => $forum->id));    // Enable attachments again.
2100  
2101          self::setAdminUser();
2102          $result = mod_forum_external::can_add_discussion($forum->id);
2103          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2104          $this->assertTrue($result['status']);
2105          $this->assertTrue($result['canpindiscussions']);
2106          $this->assertTrue($result['cancreateattachment']);
2107      }
2108  
2109      /*
2110       * A basic test to make sure users cannot post to forum after the cutoff date.
2111       */
2112      public function test_can_add_discussion_after_cutoff() {
2113          $this->resetAfterTest(true);
2114  
2115          // Create courses to add the modules.
2116          $course = self::getDataGenerator()->create_course();
2117  
2118          $user = self::getDataGenerator()->create_user();
2119  
2120          // Create a forum with cutoff date set to a past date.
2121          $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'cutoffdate' => time() - 1]);
2122  
2123          // User with no mod/forum:canoverridecutoff capability.
2124          self::setUser($user);
2125          $this->getDataGenerator()->enrol_user($user->id, $course->id);
2126  
2127          $result = mod_forum_external::can_add_discussion($forum->id);
2128          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2129          $this->assertFalse($result['status']);
2130  
2131          self::setAdminUser();
2132          $result = mod_forum_external::can_add_discussion($forum->id);
2133          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2134          $this->assertTrue($result['status']);
2135      }
2136  
2137      /**
2138       * Test get forum posts discussions including rating information.
2139       */
2140      public function test_mod_forum_get_forum_discussion_rating_information() {
2141          global $DB, $CFG;
2142          require_once($CFG->dirroot . '/rating/lib.php');
2143  
2144          $this->resetAfterTest(true);
2145  
2146          $user1 = self::getDataGenerator()->create_user();
2147          $user2 = self::getDataGenerator()->create_user();
2148          $user3 = self::getDataGenerator()->create_user();
2149          $teacher = self::getDataGenerator()->create_user();
2150  
2151          // Create course to add the module.
2152          $course = self::getDataGenerator()->create_course();
2153  
2154          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2155          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
2156          $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual');
2157          $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual');
2158          $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual');
2159          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
2160  
2161          // Create the forum.
2162          $record = new stdClass();
2163          $record->course = $course->id;
2164          // Set Aggregate type = Average of ratings.
2165          $record->assessed = RATING_AGGREGATE_AVERAGE;
2166          $record->scale = 100;
2167          $forum = self::getDataGenerator()->create_module('forum', $record);
2168          $context = context_module::instance($forum->cmid);
2169  
2170          // Add discussion to the forum.
2171          $record = new stdClass();
2172          $record->course = $course->id;
2173          $record->userid = $user1->id;
2174          $record->forum = $forum->id;
2175          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2176  
2177          // Retrieve the first post.
2178          $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2179  
2180          // Rate the discussion as user2.
2181          $rating1 = new stdClass();
2182          $rating1->contextid = $context->id;
2183          $rating1->component = 'mod_forum';
2184          $rating1->ratingarea = 'post';
2185          $rating1->itemid = $post->id;
2186          $rating1->rating = 50;
2187          $rating1->scaleid = 100;
2188          $rating1->userid = $user2->id;
2189          $rating1->timecreated = time();
2190          $rating1->timemodified = time();
2191          $rating1->id = $DB->insert_record('rating', $rating1);
2192  
2193          // Rate the discussion as user3.
2194          $rating2 = new stdClass();
2195          $rating2->contextid = $context->id;
2196          $rating2->component = 'mod_forum';
2197          $rating2->ratingarea = 'post';
2198          $rating2->itemid = $post->id;
2199          $rating2->rating = 100;
2200          $rating2->scaleid = 100;
2201          $rating2->userid = $user3->id;
2202          $rating2->timecreated = time() + 1;
2203          $rating2->timemodified = time() + 1;
2204          $rating2->id = $DB->insert_record('rating', $rating2);
2205  
2206          // Retrieve the rating for the post as student.
2207          $this->setUser($user1);
2208          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2209          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2210          $this->assertCount(1, $posts['ratinginfo']['ratings']);
2211          $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
2212          $this->assertFalse($posts['ratinginfo']['canviewall']);
2213          $this->assertFalse($posts['ratinginfo']['ratings'][0]['canrate']);
2214          $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
2215          $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
2216  
2217          // Retrieve the rating for the post as teacher.
2218          $this->setUser($teacher);
2219          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2220          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2221          $this->assertCount(1, $posts['ratinginfo']['ratings']);
2222          $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
2223          $this->assertTrue($posts['ratinginfo']['canviewall']);
2224          $this->assertTrue($posts['ratinginfo']['ratings'][0]['canrate']);
2225          $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
2226          $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
2227      }
2228  
2229      /**
2230       * Test mod_forum_get_forum_access_information.
2231       */
2232      public function test_mod_forum_get_forum_access_information() {
2233          global $DB;
2234  
2235          $this->resetAfterTest(true);
2236  
2237          $student = self::getDataGenerator()->create_user();
2238          $course = self::getDataGenerator()->create_course();
2239          // Create the forum.
2240          $record = new stdClass();
2241          $record->course = $course->id;
2242          $forum = self::getDataGenerator()->create_module('forum', $record);
2243  
2244          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2245          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
2246  
2247          self::setUser($student);
2248          $result = mod_forum_external::get_forum_access_information($forum->id);
2249          $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
2250  
2251          // Check default values for capabilities.
2252          $enabledcaps = array('canviewdiscussion', 'canstartdiscussion', 'canreplypost', 'canviewrating', 'cancreateattachment',
2253              'canexportownpost', 'cancantogglefavourite', 'candeleteownpost', 'canallowforcesubscribe');
2254  
2255          unset($result['warnings']);
2256          foreach ($result as $capname => $capvalue) {
2257              if (in_array($capname, $enabledcaps)) {
2258                  $this->assertTrue($capvalue);
2259              } else {
2260                  $this->assertFalse($capvalue);
2261              }
2262          }
2263          // Now, unassign some capabilities.
2264          unassign_capability('mod/forum:deleteownpost', $studentrole->id);
2265          unassign_capability('mod/forum:allowforcesubscribe', $studentrole->id);
2266          array_pop($enabledcaps);
2267          array_pop($enabledcaps);
2268          accesslib_clear_all_caches_for_unit_testing();
2269  
2270          $result = mod_forum_external::get_forum_access_information($forum->id);
2271          $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
2272          unset($result['warnings']);
2273          foreach ($result as $capname => $capvalue) {
2274              if (in_array($capname, $enabledcaps)) {
2275                  $this->assertTrue($capvalue);
2276              } else {
2277                  $this->assertFalse($capvalue);
2278              }
2279          }
2280      }
2281  
2282      /**
2283       * Test add_discussion_post
2284       */
2285      public function test_add_discussion_post_private() {
2286          global $DB;
2287  
2288          $this->resetAfterTest(true);
2289  
2290          self::setAdminUser();
2291  
2292          // Create course to add the module.
2293          $course = self::getDataGenerator()->create_course();
2294  
2295          // Standard forum.
2296          $record = new stdClass();
2297          $record->course = $course->id;
2298          $forum = self::getDataGenerator()->create_module('forum', $record);
2299          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
2300          $forumcontext = context_module::instance($forum->cmid);
2301          $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
2302  
2303          // Create an enrol users.
2304          $student1 = self::getDataGenerator()->create_user();
2305          $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
2306          $student2 = self::getDataGenerator()->create_user();
2307          $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
2308          $teacher1 = self::getDataGenerator()->create_user();
2309          $this->getDataGenerator()->enrol_user($teacher1->id, $course->id, 'editingteacher');
2310          $teacher2 = self::getDataGenerator()->create_user();
2311          $this->getDataGenerator()->enrol_user($teacher2->id, $course->id, 'editingteacher');
2312  
2313          // Add a new discussion to the forum.
2314          self::setUser($student1);
2315          $record = new stdClass();
2316          $record->course = $course->id;
2317          $record->userid = $student1->id;
2318          $record->forum = $forum->id;
2319          $discussion = $generator->create_discussion($record);
2320  
2321          // Have the teacher reply privately.
2322          self::setUser($teacher1);
2323          $post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...', [
2324                  [
2325                      'name' => 'private',
2326                      'value' => true,
2327                  ],
2328              ]);
2329          $post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post);
2330          $privatereply = $DB->get_record('forum_posts', array('id' => $post['postid']));
2331          $this->assertEquals($student1->id, $privatereply->privatereplyto);
2332          // Bump the time of the private reply to ensure order.
2333          $privatereply->created++;
2334          $privatereply->modified = $privatereply->created;
2335          $DB->update_record('forum_posts', $privatereply);
2336  
2337          // The teacher will receive their private reply.
2338          self::setUser($teacher1);
2339          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2340          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2341          $this->assertEquals(2, count($posts['posts']));
2342          $this->assertTrue($posts['posts'][0]['isprivatereply']);
2343  
2344          // Another teacher on the course will also receive the private reply.
2345          self::setUser($teacher2);
2346          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2347          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2348          $this->assertEquals(2, count($posts['posts']));
2349          $this->assertTrue($posts['posts'][0]['isprivatereply']);
2350  
2351          // The student will receive the private reply.
2352          self::setUser($student1);
2353          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2354          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2355          $this->assertEquals(2, count($posts['posts']));
2356          $this->assertTrue($posts['posts'][0]['isprivatereply']);
2357  
2358          // Another student will not receive the private reply.
2359          self::setUser($student2);
2360          $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2361          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2362          $this->assertEquals(1, count($posts['posts']));
2363          $this->assertFalse($posts['posts'][0]['isprivatereply']);
2364  
2365          // A user cannot reply to a private reply.
2366          self::setUser($teacher2);
2367          $this->expectException('coding_exception');
2368          $post = mod_forum_external::add_discussion_post($privatereply->id, 'some subject', 'some text here...', [
2369                  'options' => [
2370                      'name' => 'private',
2371                      'value' => false,
2372                  ],
2373              ]);
2374      }
2375  
2376      /**
2377       * Test trusted text enabled.
2378       */
2379      public function test_trusted_text_enabled() {
2380          global $USER, $CFG;
2381  
2382          $this->resetAfterTest(true);
2383          $CFG->enabletrusttext = 1;
2384  
2385          $dangeroustext = '<button>Untrusted text</button>';
2386          $cleantext = 'Untrusted text';
2387  
2388          // Create courses to add the modules.
2389          $course = self::getDataGenerator()->create_course();
2390          $user1 = self::getDataGenerator()->create_user();
2391  
2392          // First forum with tracking off.
2393          $record = new stdClass();
2394          $record->course = $course->id;
2395          $record->type = 'qanda';
2396          $forum = self::getDataGenerator()->create_module('forum', $record);
2397          $context = context_module::instance($forum->cmid);
2398  
2399          // Add discussions to the forums.
2400          $discussionrecord = new stdClass();
2401          $discussionrecord->course = $course->id;
2402          $discussionrecord->userid = $user1->id;
2403          $discussionrecord->forum = $forum->id;
2404          $discussionrecord->message = $dangeroustext;
2405          $discussionrecord->messagetrust  = trusttext_trusted($context);
2406          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2407  
2408          self::setAdminUser();
2409          $discussionrecord->userid = $USER->id;
2410          $discussionrecord->messagetrust  = trusttext_trusted($context);
2411          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2412  
2413          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
2414          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
2415  
2416          $this->assertCount(2, $discussions['discussions']);
2417          $this->assertCount(0, $discussions['warnings']);
2418          // Admin message is fully trusted.
2419          $this->assertEquals(1, $discussions['discussions'][0]['messagetrust']);
2420          $this->assertEquals($dangeroustext, $discussions['discussions'][0]['message']);
2421          // Student message is not trusted.
2422          $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']);
2423          $this->assertEquals($cleantext, $discussions['discussions'][1]['message']);
2424  
2425          // Get posts now.
2426          $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
2427          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2428          // Admin message is fully trusted.
2429          $this->assertEquals(1, $posts['posts'][0]['messagetrust']);
2430          $this->assertEquals($dangeroustext, $posts['posts'][0]['message']);
2431  
2432          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
2433          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2434          // Student message is not trusted.
2435          $this->assertEquals(0, $posts['posts'][0]['messagetrust']);
2436          $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2437      }
2438  
2439      /**
2440       * Test trusted text disabled.
2441       */
2442      public function test_trusted_text_disabled() {
2443          global $USER, $CFG;
2444  
2445          $this->resetAfterTest(true);
2446          $CFG->enabletrusttext = 0;
2447  
2448          $dangeroustext = '<button>Untrusted text</button>';
2449          $cleantext = 'Untrusted text';
2450  
2451          // Create courses to add the modules.
2452          $course = self::getDataGenerator()->create_course();
2453          $user1 = self::getDataGenerator()->create_user();
2454  
2455          // First forum with tracking off.
2456          $record = new stdClass();
2457          $record->course = $course->id;
2458          $record->type = 'qanda';
2459          $forum = self::getDataGenerator()->create_module('forum', $record);
2460          $context = context_module::instance($forum->cmid);
2461  
2462          // Add discussions to the forums.
2463          $discussionrecord = new stdClass();
2464          $discussionrecord->course = $course->id;
2465          $discussionrecord->userid = $user1->id;
2466          $discussionrecord->forum = $forum->id;
2467          $discussionrecord->message = $dangeroustext;
2468          $discussionrecord->messagetrust = trusttext_trusted($context);
2469          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2470  
2471          self::setAdminUser();
2472          $discussionrecord->userid = $USER->id;
2473          $discussionrecord->messagetrust = trusttext_trusted($context);
2474          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2475  
2476          $discussions = mod_forum_external::get_forum_discussions($forum->id);
2477          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
2478  
2479          $this->assertCount(2, $discussions['discussions']);
2480          $this->assertCount(0, $discussions['warnings']);
2481          // Admin message is not trusted because enabletrusttext is disabled.
2482          $this->assertEquals(0, $discussions['discussions'][0]['messagetrust']);
2483          $this->assertEquals($cleantext, $discussions['discussions'][0]['message']);
2484          // Student message is not trusted.
2485          $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']);
2486          $this->assertEquals($cleantext, $discussions['discussions'][1]['message']);
2487  
2488          // Get posts now.
2489          $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
2490          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2491          // Admin message is not trusted because enabletrusttext is disabled.
2492          $this->assertEquals(0, $posts['posts'][0]['messagetrust']);
2493          $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2494  
2495          $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
2496          $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2497          // Student message is not trusted.
2498          $this->assertEquals(0, $posts['posts'][0]['messagetrust']);
2499          $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2500      }
2501  
2502      /**
2503       * Test delete a discussion.
2504       */
2505      public function test_delete_post_discussion() {
2506          global $DB;
2507          $this->resetAfterTest(true);
2508  
2509          // Setup test data.
2510          $course = $this->getDataGenerator()->create_course();
2511          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2512          $user = $this->getDataGenerator()->create_user();
2513          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2514          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2515  
2516          // Add a discussion.
2517          $record = new stdClass();
2518          $record->course = $course->id;
2519          $record->userid = $user->id;
2520          $record->forum = $forum->id;
2521          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2522  
2523          $this->setUser($user);
2524          $result = mod_forum_external::delete_post($discussion->firstpost);
2525          $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result);
2526          $this->assertTrue($result['status']);
2527          $this->assertEquals(0, $DB->count_records('forum_posts', array('id' => $discussion->firstpost)));
2528          $this->assertEquals(0, $DB->count_records('forum_discussions', array('id' => $discussion->id)));
2529      }
2530  
2531      /**
2532       * Test delete a post.
2533       */
2534      public function test_delete_post_post() {
2535          global $DB;
2536          $this->resetAfterTest(true);
2537  
2538          // Setup test data.
2539          $course = $this->getDataGenerator()->create_course();
2540          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2541          $user = $this->getDataGenerator()->create_user();
2542          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2543          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2544  
2545          // Add a discussion.
2546          $record = new stdClass();
2547          $record->course = $course->id;
2548          $record->userid = $user->id;
2549          $record->forum = $forum->id;
2550          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2551          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2552  
2553          // Add a post.
2554          $record = new stdClass();
2555          $record->course = $course->id;
2556          $record->userid = $user->id;
2557          $record->forum = $forum->id;
2558          $record->discussion = $discussion->id;
2559          $record->parent = $parentpost->id;
2560          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
2561  
2562          $this->setUser($user);
2563          $result = mod_forum_external::delete_post($post->id);
2564          $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result);
2565          $this->assertTrue($result['status']);
2566          $this->assertEquals(1, $DB->count_records('forum_posts', array('discussion' => $discussion->id)));
2567          $this->assertEquals(1, $DB->count_records('forum_discussions', array('id' => $discussion->id)));
2568      }
2569  
2570      /**
2571       * Test delete a different user post.
2572       */
2573      public function test_delete_post_other_user_post() {
2574          global $DB;
2575          $this->resetAfterTest(true);
2576  
2577          // Setup test data.
2578          $course = $this->getDataGenerator()->create_course();
2579          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2580          $user = $this->getDataGenerator()->create_user();
2581          $otheruser = $this->getDataGenerator()->create_user();
2582          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2583          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2584          self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
2585  
2586          // Add a discussion.
2587          $record = array();
2588          $record['course'] = $course->id;
2589          $record['forum'] = $forum->id;
2590          $record['userid'] = $user->id;
2591          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2592          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2593  
2594          // Add a post.
2595          $record = new stdClass();
2596          $record->course = $course->id;
2597          $record->userid = $user->id;
2598          $record->forum = $forum->id;
2599          $record->discussion = $discussion->id;
2600          $record->parent = $parentpost->id;
2601          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
2602  
2603          $this->setUser($otheruser);
2604          $this->expectExceptionMessage(get_string('cannotdeletepost', 'forum'));
2605          mod_forum_external::delete_post($post->id);
2606      }
2607  
2608      /*
2609       * Test get forum posts by user id.
2610       */
2611      public function test_mod_forum_get_discussion_posts_by_userid() {
2612          global $DB;
2613          $this->resetAfterTest(true);
2614  
2615          $urlfactory = mod_forum\local\container::get_url_factory();
2616          $entityfactory = mod_forum\local\container::get_entity_factory();
2617          $vaultfactory = mod_forum\local\container::get_vault_factory();
2618          $postvault = $vaultfactory->get_post_vault();
2619          $legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory();
2620          $legacypostmapper = $legacydatamapper->get_post_data_mapper();
2621  
2622          // Create course to add the module.
2623          $course1 = self::getDataGenerator()->create_course();
2624  
2625          $user1 = self::getDataGenerator()->create_user();
2626          $user1entity = $entityfactory->get_author_from_stdclass($user1);
2627          $exporteduser1 = [
2628              'id' => (int) $user1->id,
2629              'fullname' => fullname($user1),
2630              'groups' => [],
2631              'urls' => [
2632                  'profile' => $urlfactory->get_author_profile_url($user1entity, $course1->id)->out(false),
2633                  'profileimage' => $urlfactory->get_author_profile_image_url($user1entity),
2634              ],
2635              'isdeleted' => false,
2636          ];
2637          // Create a bunch of other users to post.
2638          $user2 = self::getDataGenerator()->create_user();
2639          $user2entity = $entityfactory->get_author_from_stdclass($user2);
2640          $exporteduser2 = [
2641              'id' => (int) $user2->id,
2642              'fullname' => fullname($user2),
2643              'groups' => [],
2644              'urls' => [
2645                  'profile' => $urlfactory->get_author_profile_url($user2entity, $course1->id)->out(false),
2646                  'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
2647              ],
2648              'isdeleted' => false,
2649          ];
2650          $user2->fullname = $exporteduser2['fullname'];
2651  
2652          $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
2653  
2654          // Set the first created user to the test user.
2655          self::setUser($user1);
2656  
2657          // Forum with tracking off.
2658          $record = new stdClass();
2659          $record->course = $course1->id;
2660          $forum1 = self::getDataGenerator()->create_module('forum', $record);
2661          $forum1context = context_module::instance($forum1->cmid);
2662  
2663          // Add discussions to the forums.
2664          $time = time();
2665          $record = new stdClass();
2666          $record->course = $course1->id;
2667          $record->userid = $user1->id;
2668          $record->forum = $forum1->id;
2669          $record->timemodified = $time + 100;
2670          $discussion1 = $forumgenerator->create_discussion($record);
2671          $discussion1firstpost = $postvault->get_first_post_for_discussion_ids([$discussion1->id]);
2672          $discussion1firstpost = $discussion1firstpost[$discussion1->firstpost];
2673          $discussion1firstpostobject = $legacypostmapper->to_legacy_object($discussion1firstpost);
2674  
2675          $record = new stdClass();
2676          $record->course = $course1->id;
2677          $record->userid = $user1->id;
2678          $record->forum = $forum1->id;
2679          $record->timemodified = $time + 200;
2680          $discussion2 = $forumgenerator->create_discussion($record);
2681          $discussion2firstpost = $postvault->get_first_post_for_discussion_ids([$discussion2->id]);
2682          $discussion2firstpost = $discussion2firstpost[$discussion2->firstpost];
2683          $discussion2firstpostobject = $legacypostmapper->to_legacy_object($discussion2firstpost);
2684  
2685          // Add 1 reply to the discussion 1 from a different user.
2686          $record = new stdClass();
2687          $record->discussion = $discussion1->id;
2688          $record->parent = $discussion1->firstpost;
2689          $record->userid = $user2->id;
2690          $discussion1reply1 = $forumgenerator->create_post($record);
2691          $filename = 'shouldbeanimage.jpg';
2692          // Add a fake inline image to the post.
2693          $filerecordinline = array(
2694                  'contextid' => $forum1context->id,
2695                  'component' => 'mod_forum',
2696                  'filearea'  => 'post',
2697                  'itemid'    => $discussion1reply1->id,
2698                  'filepath'  => '/',
2699                  'filename'  => $filename,
2700          );
2701          $fs = get_file_storage();
2702          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2703  
2704          // Add 1 reply to the discussion 2 from a different user.
2705          $record = new stdClass();
2706          $record->discussion = $discussion2->id;
2707          $record->parent = $discussion2->firstpost;
2708          $record->userid = $user2->id;
2709          $discussion2reply1 = $forumgenerator->create_post($record);
2710          $filename = 'shouldbeanimage.jpg';
2711          // Add a fake inline image to the post.
2712          $filerecordinline = array(
2713                  'contextid' => $forum1context->id,
2714                  'component' => 'mod_forum',
2715                  'filearea'  => 'post',
2716                  'itemid'    => $discussion2reply1->id,
2717                  'filepath'  => '/',
2718                  'filename'  => $filename,
2719          );
2720          $fs = get_file_storage();
2721          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2722  
2723          // Following line enrol and assign default role id to the user.
2724          // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
2725          $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
2726          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
2727          // Changed display period for the discussions in past.
2728          $discussion = new \stdClass();
2729          $discussion->id = $discussion1->id;
2730          $discussion->timestart = $time - 200;
2731          $discussion->timeend = $time - 100;
2732          $DB->update_record('forum_discussions', $discussion);
2733          $discussion = new \stdClass();
2734          $discussion->id = $discussion2->id;
2735          $discussion->timestart = $time - 200;
2736          $discussion->timeend = $time - 100;
2737          $DB->update_record('forum_discussions', $discussion);
2738          // Create what we expect to be returned when querying the discussion.
2739          $expectedposts = array(
2740              'discussions' => array(),
2741              'warnings' => array(),
2742          );
2743  
2744          $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
2745          $isolatedurluser->params(['parent' => $discussion1reply1->id]);
2746          $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1firstpostobject->discussion);
2747          $isolatedurlparent->params(['parent' => $discussion1firstpostobject->id]);
2748  
2749          $expectedposts['discussions'][0] = [
2750              'name' => $discussion1->name,
2751              'id' => $discussion1->id,
2752              'timecreated' => $discussion1firstpost->get_time_created(),
2753              'authorfullname' => $user1entity->get_full_name(),
2754              'posts' => [
2755                  'userposts' => [
2756                      [
2757                          'id' => $discussion1reply1->id,
2758                          'discussionid' => $discussion1reply1->discussion,
2759                          'parentid' => $discussion1reply1->parent,
2760                          'hasparent' => true,
2761                          'timecreated' => $discussion1reply1->created,
2762                          'subject' => $discussion1reply1->subject,
2763                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
2764                          'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
2765                          $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
2766                          'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2767                          'unread' => null,
2768                          'isdeleted' => false,
2769                          'isprivatereply' => false,
2770                          'haswordcount' => false,
2771                          'wordcount' => null,
2772                          'author' => $exporteduser2,
2773                          'attachments' => [],
2774                          'tags' => [],
2775                          'html' => [
2776                              'rating' => null,
2777                              'taglist' => null,
2778                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2779                                  (object)$exporteduser2, $discussion1reply1->created)
2780                          ],
2781                          'charcount' => null,
2782                          'capabilities' => [
2783                              'view' => true,
2784                              'edit' => true,
2785                              'delete' => true,
2786                              'split' => true,
2787                              'reply' => true,
2788                              'export' => false,
2789                              'controlreadstatus' => false,
2790                              'canreplyprivately' => true,
2791                              'selfenrol' => false
2792                          ],
2793                          'urls' => [
2794                              'view' => $urlfactory->get_view_post_url_from_post_id(
2795                                  $discussion1reply1->discussion, $discussion1reply1->id)->out(false),
2796                              'viewisolated' => $isolatedurluser->out(false),
2797                              'viewparent' => $urlfactory->get_view_post_url_from_post_id(
2798                                  $discussion1reply1->discussion, $discussion1reply1->parent)->out(false),
2799                              'edit' => (new moodle_url('/mod/forum/post.php', [
2800                                  'edit' => $discussion1reply1->id
2801                              ]))->out(false),
2802                              'delete' => (new moodle_url('/mod/forum/post.php', [
2803                                  'delete' => $discussion1reply1->id
2804                              ]))->out(false),
2805                              'split' => (new moodle_url('/mod/forum/post.php', [
2806                                  'prune' => $discussion1reply1->id
2807                              ]))->out(false),
2808                              'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2809                                  'reply' => $discussion1reply1->id
2810                              ]))->out(false),
2811                              'export' => null,
2812                              'markasread' => null,
2813                              'markasunread' => null,
2814                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2815                                  $discussion1reply1->discussion)->out(false),
2816                          ],
2817                      ]
2818                  ],
2819                  'parentposts' => [
2820                      [
2821                          'id' => $discussion1firstpostobject->id,
2822                          'discussionid' => $discussion1firstpostobject->discussion,
2823                          'parentid' => null,
2824                          'hasparent' => false,
2825                          'timecreated' => $discussion1firstpostobject->created,
2826                          'subject' => $discussion1firstpostobject->subject,
2827                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion1firstpostobject->subject}",
2828                          'message' => file_rewrite_pluginfile_urls($discussion1firstpostobject->message, 'pluginfile.php',
2829                              $forum1context->id, 'mod_forum', 'post', $discussion1firstpostobject->id),
2830                          'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2831                          'unread' => null,
2832                          'isdeleted' => false,
2833                          'isprivatereply' => false,
2834                          'haswordcount' => false,
2835                          'wordcount' => null,
2836                          'author' => $exporteduser1,
2837                          'attachments' => [],
2838                          'tags' => [],
2839                          'html' => [
2840                              'rating' => null,
2841                              'taglist' => null,
2842                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2843                                  (object)$exporteduser1, $discussion1firstpostobject->created)
2844                          ],
2845                          'charcount' => null,
2846                          'capabilities' => [
2847                              'view' => true,
2848                              'edit' => true,
2849                              'delete' => true,
2850                              'split' => false,
2851                              'reply' => true,
2852                              'export' => false,
2853                              'controlreadstatus' => false,
2854                              'canreplyprivately' => true,
2855                              'selfenrol' => false
2856                          ],
2857                          'urls' => [
2858                              'view' => $urlfactory->get_view_post_url_from_post_id(
2859                                  $discussion1firstpostobject->discussion, $discussion1firstpostobject->id)->out(false),
2860                              'viewisolated' => $isolatedurlparent->out(false),
2861                              'viewparent' => null,
2862                              'edit' => (new moodle_url('/mod/forum/post.php', [
2863                                  'edit' => $discussion1firstpostobject->id
2864                              ]))->out(false),
2865                              'delete' => (new moodle_url('/mod/forum/post.php', [
2866                                  'delete' => $discussion1firstpostobject->id
2867                              ]))->out(false),
2868                              'split' => null,
2869                              'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2870                                  'reply' => $discussion1firstpostobject->id
2871                              ]))->out(false),
2872                              'export' => null,
2873                              'markasread' => null,
2874                              'markasunread' => null,
2875                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2876                                  $discussion1firstpostobject->discussion)->out(false),
2877                          ],
2878                      ]
2879                  ],
2880              ],
2881          ];
2882  
2883          $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2reply1->discussion);
2884          $isolatedurluser->params(['parent' => $discussion2reply1->id]);
2885          $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2firstpostobject->discussion);
2886          $isolatedurlparent->params(['parent' => $discussion2firstpostobject->id]);
2887  
2888          $expectedposts['discussions'][1] = [
2889              'name' => $discussion2->name,
2890              'id' => $discussion2->id,
2891              'timecreated' => $discussion2firstpost->get_time_created(),
2892              'authorfullname' => $user1entity->get_full_name(),
2893              'posts' => [
2894                  'userposts' => [
2895                      [
2896                          'id' => $discussion2reply1->id,
2897                          'discussionid' => $discussion2reply1->discussion,
2898                          'parentid' => $discussion2reply1->parent,
2899                          'hasparent' => true,
2900                          'timecreated' => $discussion2reply1->created,
2901                          'subject' => $discussion2reply1->subject,
2902                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion2reply1->subject}",
2903                          'message' => file_rewrite_pluginfile_urls($discussion2reply1->message, 'pluginfile.php',
2904                              $forum1context->id, 'mod_forum', 'post', $discussion2reply1->id),
2905                          'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2906                          'unread' => null,
2907                          'isdeleted' => false,
2908                          'isprivatereply' => false,
2909                          'haswordcount' => false,
2910                          'wordcount' => null,
2911                          'author' => $exporteduser2,
2912                          'attachments' => [],
2913                          'tags' => [],
2914                          'html' => [
2915                              'rating' => null,
2916                              'taglist' => null,
2917                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2918                                  (object)$exporteduser2, $discussion2reply1->created)
2919                          ],
2920                          'charcount' => null,
2921                          'capabilities' => [
2922                              'view' => true,
2923                              'edit' => true,
2924                              'delete' => true,
2925                              'split' => true,
2926                              'reply' => true,
2927                              'export' => false,
2928                              'controlreadstatus' => false,
2929                              'canreplyprivately' => true,
2930                              'selfenrol' => false
2931                          ],
2932                          'urls' => [
2933                              'view' => $urlfactory->get_view_post_url_from_post_id(
2934                                  $discussion2reply1->discussion, $discussion2reply1->id)->out(false),
2935                              'viewisolated' => $isolatedurluser->out(false),
2936                              'viewparent' => $urlfactory->get_view_post_url_from_post_id(
2937                                  $discussion2reply1->discussion, $discussion2reply1->parent)->out(false),
2938                              'edit' => (new moodle_url('/mod/forum/post.php', [
2939                                  'edit' => $discussion2reply1->id
2940                              ]))->out(false),
2941                              'delete' => (new moodle_url('/mod/forum/post.php', [
2942                                  'delete' => $discussion2reply1->id
2943                              ]))->out(false),
2944                              'split' => (new moodle_url('/mod/forum/post.php', [
2945                                  'prune' => $discussion2reply1->id
2946                              ]))->out(false),
2947                              'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2948                                  'reply' => $discussion2reply1->id
2949                              ]))->out(false),
2950                              'export' => null,
2951                              'markasread' => null,
2952                              'markasunread' => null,
2953                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2954                                  $discussion2reply1->discussion)->out(false),
2955                          ],
2956                      ]
2957                  ],
2958                  'parentposts' => [
2959                      [
2960                          'id' => $discussion2firstpostobject->id,
2961                          'discussionid' => $discussion2firstpostobject->discussion,
2962                          'parentid' => null,
2963                          'hasparent' => false,
2964                          'timecreated' => $discussion2firstpostobject->created,
2965                          'subject' => $discussion2firstpostobject->subject,
2966                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion2firstpostobject->subject}",
2967                          'message' => file_rewrite_pluginfile_urls($discussion2firstpostobject->message, 'pluginfile.php',
2968                              $forum1context->id, 'mod_forum', 'post', $discussion2firstpostobject->id),
2969                          'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2970                          'unread' => null,
2971                          'isdeleted' => false,
2972                          'isprivatereply' => false,
2973                          'haswordcount' => false,
2974                          'wordcount' => null,
2975                          'author' => $exporteduser1,
2976                          'attachments' => [],
2977                          'tags' => [],
2978                          'html' => [
2979                              'rating' => null,
2980                              'taglist' => null,
2981                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2982                                  (object)$exporteduser1, $discussion2firstpostobject->created)
2983                          ],
2984                          'charcount' => null,
2985                          'capabilities' => [
2986                              'view' => true,
2987                              'edit' => true,
2988                              'delete' => true,
2989                              'split' => false,
2990                              'reply' => true,
2991                              'export' => false,
2992                              'controlreadstatus' => false,
2993                              'canreplyprivately' => true,
2994                              'selfenrol' => false
2995                          ],
2996                          'urls' => [
2997                              'view' => $urlfactory->get_view_post_url_from_post_id(
2998                                  $discussion2firstpostobject->discussion, $discussion2firstpostobject->id)->out(false),
2999                              'viewisolated' => $isolatedurlparent->out(false),
3000                              'viewparent' => null,
3001                              'edit' => (new moodle_url('/mod/forum/post.php', [
3002                                  'edit' => $discussion2firstpostobject->id
3003                              ]))->out(false),
3004                              'delete' => (new moodle_url('/mod/forum/post.php', [
3005                                  'delete' => $discussion2firstpostobject->id
3006                              ]))->out(false),
3007                              'split' => null,
3008                              'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
3009                                  'reply' => $discussion2firstpostobject->id
3010                              ]))->out(false),
3011                              'export' => null,
3012                              'markasread' => null,
3013                              'markasunread' => null,
3014                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
3015                                  $discussion2firstpostobject->discussion)->out(false),
3016  
3017                          ]
3018                      ],
3019                  ]
3020              ],
3021          ];
3022  
3023          // Test discussions with one additional post each (total 2 posts).
3024          // Also testing that we get the parent posts too.
3025          $discussions = mod_forum_external::get_discussion_posts_by_userid($user2->id, $forum1->cmid, 'modified', 'DESC');
3026          $discussions = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_by_userid_returns(), $discussions);
3027  
3028          $this->assertEquals(2, count($discussions['discussions']));
3029  
3030          $this->assertEquals($expectedposts, $discussions);
3031      }
3032  
3033      /**
3034       * Test get_discussion_post a discussion.
3035       */
3036      public function test_get_discussion_post_discussion() {
3037          global $DB;
3038          $this->resetAfterTest(true);
3039          // Setup test data.
3040          $course = $this->getDataGenerator()->create_course();
3041          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3042          $user = $this->getDataGenerator()->create_user();
3043          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3044          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3045          // Add a discussion.
3046          $record = new stdClass();
3047          $record->course = $course->id;
3048          $record->userid = $user->id;
3049          $record->forum = $forum->id;
3050          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3051          $this->setUser($user);
3052          $result = mod_forum_external::get_discussion_post($discussion->firstpost);
3053          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3054          $this->assertEquals($discussion->firstpost, $result['post']['id']);
3055          $this->assertFalse($result['post']['hasparent']);
3056          $this->assertEquals($discussion->message, $result['post']['message']);
3057      }
3058  
3059      /**
3060       * Test get_discussion_post a post.
3061       */
3062      public function test_get_discussion_post_post() {
3063          global $DB;
3064          $this->resetAfterTest(true);
3065          // Setup test data.
3066          $course = $this->getDataGenerator()->create_course();
3067          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3068          $user = $this->getDataGenerator()->create_user();
3069          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3070          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3071          // Add a discussion.
3072          $record = new stdClass();
3073          $record->course = $course->id;
3074          $record->userid = $user->id;
3075          $record->forum = $forum->id;
3076          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3077          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3078          // Add a post.
3079          $record = new stdClass();
3080          $record->course = $course->id;
3081          $record->userid = $user->id;
3082          $record->forum = $forum->id;
3083          $record->discussion = $discussion->id;
3084          $record->parent = $parentpost->id;
3085          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3086          $this->setUser($user);
3087          $result = mod_forum_external::get_discussion_post($post->id);
3088          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3089          $this->assertEquals($post->id, $result['post']['id']);
3090          $this->assertTrue($result['post']['hasparent']);
3091          $this->assertEquals($post->message, $result['post']['message']);
3092      }
3093  
3094      /**
3095       * Test get_discussion_post a different user post.
3096       */
3097      public function test_get_discussion_post_other_user_post() {
3098          global $DB;
3099          $this->resetAfterTest(true);
3100          // Setup test data.
3101          $course = $this->getDataGenerator()->create_course();
3102          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3103          $user = $this->getDataGenerator()->create_user();
3104          $otheruser = $this->getDataGenerator()->create_user();
3105          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3106          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3107          self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
3108          // Add a discussion.
3109          $record = array();
3110          $record['course'] = $course->id;
3111          $record['forum'] = $forum->id;
3112          $record['userid'] = $user->id;
3113          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3114          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3115          // Add a post.
3116          $record = new stdClass();
3117          $record->course = $course->id;
3118          $record->userid = $user->id;
3119          $record->forum = $forum->id;
3120          $record->discussion = $discussion->id;
3121          $record->parent = $parentpost->id;
3122          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3123          // Check other user post.
3124          $this->setUser($otheruser);
3125          $result = mod_forum_external::get_discussion_post($post->id);
3126          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3127          $this->assertEquals($post->id, $result['post']['id']);
3128          $this->assertTrue($result['post']['hasparent']);
3129          $this->assertEquals($post->message, $result['post']['message']);
3130      }
3131  
3132      /**
3133       * Test prepare_draft_area_for_post a different user post.
3134       */
3135      public function test_prepare_draft_area_for_post() {
3136          global $DB;
3137          $this->resetAfterTest(true);
3138          // Setup test data.
3139          $course = $this->getDataGenerator()->create_course();
3140          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3141          $user = $this->getDataGenerator()->create_user();
3142          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3143          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3144          // Add a discussion.
3145          $record = array();
3146          $record['course'] = $course->id;
3147          $record['forum'] = $forum->id;
3148          $record['userid'] = $user->id;
3149          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3150          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3151          // Add a post.
3152          $record = new stdClass();
3153          $record->course = $course->id;
3154          $record->userid = $user->id;
3155          $record->forum = $forum->id;
3156          $record->discussion = $discussion->id;
3157          $record->parent = $parentpost->id;
3158          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3159  
3160          // Add some files only in the attachment area.
3161          $filename = 'faketxt.txt';
3162          $filerecordinline = array(
3163              'contextid' => context_module::instance($forum->cmid)->id,
3164              'component' => 'mod_forum',
3165              'filearea'  => 'attachment',
3166              'itemid'    => $post->id,
3167              'filepath'  => '/',
3168              'filename'  => $filename,
3169          );
3170          $fs = get_file_storage();
3171          $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
3172          $filerecordinline['filename'] = 'otherfaketxt.txt';
3173          $fs->create_file_from_string($filerecordinline, 'fake txt contents 2.');
3174  
3175          $this->setUser($user);
3176  
3177          // Check attachment area.
3178          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment');
3179          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3180          $this->assertCount(2, $result['files']);
3181          $this->assertEquals($filename, $result['files'][0]['filename']);
3182          $this->assertCount(5, $result['areaoptions']);
3183          $this->assertEmpty($result['messagetext']);
3184  
3185          // Check again using existing draft item id.
3186          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid']);
3187          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3188          $this->assertCount(2, $result['files']);
3189  
3190          // Keep only certain files in the area.
3191          $filestokeep = array(array('filename' => $filename, 'filepath' => '/'));
3192          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid'], $filestokeep);
3193          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3194          $this->assertCount(1, $result['files']);
3195          $this->assertEquals($filename, $result['files'][0]['filename']);
3196  
3197          // Check editor (post) area.
3198          $filerecordinline['filearea'] = 'post';
3199          $filerecordinline['filename'] = 'fakeimage.png';
3200          $fs->create_file_from_string($filerecordinline, 'fake image.');
3201          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'post');
3202          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3203          $this->assertCount(1, $result['files']);
3204          $this->assertEquals($filerecordinline['filename'], $result['files'][0]['filename']);
3205          $this->assertCount(5, $result['areaoptions']);
3206          $this->assertEquals($post->message, $result['messagetext']);
3207      }
3208  
3209      /**
3210       * Test update_discussion_post with a discussion.
3211       */
3212      public function test_update_discussion_post_discussion() {
3213          global $DB, $USER;
3214          $this->resetAfterTest(true);
3215          // Setup test data.
3216          $course = $this->getDataGenerator()->create_course();
3217          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3218  
3219          $this->setAdminUser();
3220  
3221          // Add a discussion.
3222          $record = new stdClass();
3223          $record->course = $course->id;
3224          $record->userid = $USER->id;
3225          $record->forum = $forum->id;
3226          $record->pinned = FORUM_DISCUSSION_UNPINNED;
3227          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3228  
3229          $subject = 'Hey subject updated';
3230          $message = 'Hey message updated';
3231          $messageformat = FORMAT_HTML;
3232          $options = [
3233              ['name' => 'pinned', 'value' => true],
3234          ];
3235  
3236          $result = mod_forum_external::update_discussion_post($discussion->firstpost, $subject, $message, $messageformat,
3237              $options);
3238          $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
3239          $this->assertTrue($result['status']);
3240  
3241          // Get the post from WS.
3242          $result = mod_forum_external::get_discussion_post($discussion->firstpost);
3243          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3244          $this->assertEquals($subject, $result['post']['subject']);
3245          $this->assertEquals($message, $result['post']['message']);
3246          $this->assertEquals($messageformat, $result['post']['messageformat']);
3247  
3248          // Get discussion object from DB.
3249          $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
3250          $this->assertEquals($subject, $discussion->name);   // Check discussion subject.
3251          $this->assertEquals(FORUM_DISCUSSION_PINNED, $discussion->pinned);  // Check discussion pinned.
3252      }
3253  
3254      /**
3255       * Test update_discussion_post with a post.
3256       */
3257      public function test_update_discussion_post_post() {
3258          global $DB, $USER;
3259          $this->resetAfterTest(true);
3260          // Setup test data.
3261          $course = $this->getDataGenerator()->create_course();
3262          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3263          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
3264          $user = $this->getDataGenerator()->create_user();
3265          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3266          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3267  
3268          $this->setUser($user);
3269          // Enable auto subscribe discussion.
3270          $USER->autosubscribe = true;
3271  
3272          // Add a discussion.
3273          $record = new stdClass();
3274          $record->course = $course->id;
3275          $record->userid = $user->id;
3276          $record->forum = $forum->id;
3277          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3278  
3279          // Add a post via WS (so discussion subscription works).
3280          $result = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
3281          $newpost = $result['post'];
3282          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
3283  
3284          // Add files in the different areas.
3285          $draftidattach = file_get_unused_draft_itemid();
3286          $filerecordinline = array(
3287              'contextid' => context_user::instance($user->id)->id,
3288              'component' => 'user',
3289              'filearea'  => 'draft',
3290              'itemid'    => $draftidattach,
3291              'filepath'  => '/',
3292              'filename'  => 'faketxt.txt',
3293          );
3294          $fs = get_file_storage();
3295          $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
3296  
3297          // Create files in post area (inline).
3298          $draftidinlineattach = file_get_unused_draft_itemid();
3299          $filerecordinline['itemid'] = $draftidinlineattach;
3300          $filerecordinline['filename'] = 'fakeimage.png';
3301          $fs->create_file_from_string($filerecordinline, 'img...');
3302  
3303          // Do not update subject.
3304          $message = 'Hey message updated';
3305          $messageformat = FORMAT_HTML;
3306          $options = [
3307              ['name' => 'discussionsubscribe', 'value' => false],
3308              ['name' => 'inlineattachmentsid', 'value' => $draftidinlineattach],
3309              ['name' => 'attachmentsid', 'value' => $draftidattach],
3310          ];
3311  
3312          $result = mod_forum_external::update_discussion_post($newpost->id, '', $message, $messageformat, $options);
3313          $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
3314          $this->assertTrue($result['status']);
3315          // Check subscription status.
3316          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
3317  
3318          // Get the post from WS.
3319          $result = mod_forum_external::get_forum_discussion_posts($discussion->id);
3320          $result = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $result);
3321          $found = false;
3322          foreach ($result['posts'] as $post) {
3323              if ($post['id'] == $newpost->id) {
3324                  $this->assertEquals($newpost->subject, $post['subject']);
3325                  $this->assertEquals($message, $post['message']);
3326                  $this->assertEquals($messageformat, $post['messageformat']);
3327                  $this->assertCount(1, $post['messageinlinefiles']);
3328                  $this->assertEquals('fakeimage.png', $post['messageinlinefiles'][0]['filename']);
3329                  $this->assertCount(1, $post['attachments']);
3330                  $this->assertEquals('faketxt.txt', $post['attachments'][0]['filename']);
3331                  $found = true;
3332              }
3333          }
3334          $this->assertTrue($found);
3335      }
3336  
3337      /**
3338       * Test update_discussion_post with other user post (no permissions).
3339       */
3340      public function test_update_discussion_post_other_user_post() {
3341          global $DB, $USER;
3342          $this->resetAfterTest(true);
3343          // Setup test data.
3344          $course = $this->getDataGenerator()->create_course();
3345          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3346          $user = $this->getDataGenerator()->create_user();
3347          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3348          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3349  
3350          $this->setAdminUser();
3351          // Add a discussion.
3352          $record = new stdClass();
3353          $record->course = $course->id;
3354          $record->userid = $USER->id;
3355          $record->forum = $forum->id;
3356          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3357  
3358          // Add a post.
3359          $record = new stdClass();
3360          $record->course = $course->id;
3361          $record->userid = $USER->id;
3362          $record->forum = $forum->id;
3363          $record->discussion = $discussion->id;
3364          $record->parent = $discussion->firstpost;
3365          $newpost = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3366  
3367          $this->setUser($user);
3368          $subject = 'Hey subject updated';
3369          $message = 'Hey message updated';
3370          $messageformat = FORMAT_HTML;
3371  
3372          $this->expectExceptionMessage(get_string('cannotupdatepost', 'forum'));
3373          mod_forum_external::update_discussion_post($newpost->id, $subject, $message, $messageformat);
3374      }
3375  
3376      /**
3377       * Test that we can update the subject of a post to the string '0'
3378       */
3379      public function test_update_discussion_post_set_subject_to_zero(): void {
3380          global $DB, $USER;
3381  
3382          $this->resetAfterTest(true);
3383          $this->setAdminUser();
3384  
3385          // Setup test data.
3386          $course = $this->getDataGenerator()->create_course();
3387          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
3388  
3389          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [
3390              'userid' => $USER->id,
3391              'course' => $course->id,
3392              'forum' => $forum->id,
3393              'name' => 'Test discussion subject',
3394          ]);
3395  
3396          // Update discussion post subject.
3397          $result = external_api::clean_returnvalue(
3398              mod_forum_external::update_discussion_post_returns(),
3399              mod_forum_external::update_discussion_post($discussion->firstpost, '0')
3400          );
3401          $this->assertTrue($result['status']);
3402  
3403          // Get updated discussion post subject from DB.
3404          $postsubject = $DB->get_field('forum_posts', 'subject', ['id' => $discussion->firstpost]);
3405          $this->assertEquals('0', $postsubject);
3406      }
3407  
3408      /**
3409       * Test that we can update the message of a post to the string '0'
3410       */
3411      public function test_update_discussion_post_set_message_to_zero(): void {
3412          global $DB, $USER;
3413  
3414          $this->resetAfterTest(true);
3415          $this->setAdminUser();
3416  
3417          // Setup test data.
3418          $course = $this->getDataGenerator()->create_course();
3419          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
3420  
3421          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [
3422              'userid' => $USER->id,
3423              'course' => $course->id,
3424              'forum' => $forum->id,
3425              'message' => 'Test discussion message',
3426              'messageformat' => FORMAT_HTML,
3427          ]);
3428  
3429          // Update discussion post message.
3430          $result = external_api::clean_returnvalue(
3431              mod_forum_external::update_discussion_post_returns(),
3432              mod_forum_external::update_discussion_post($discussion->firstpost, '', '0', FORMAT_HTML)
3433          );
3434          $this->assertTrue($result['status']);
3435  
3436          // Get updated discussion post subject from DB.
3437          $postmessage = $DB->get_field('forum_posts', 'message', ['id' => $discussion->firstpost]);
3438          $this->assertEquals('0', $postmessage);
3439      }
3440  
3441      /**
3442       * Test that we can update the message format of a post to {@see FORMAT_MOODLE}
3443       */
3444      public function test_update_discussion_post_set_message_format_moodle(): void {
3445          global $DB, $USER;
3446  
3447          $this->resetAfterTest(true);
3448          $this->setAdminUser();
3449  
3450          // Setup test data.
3451          $course = $this->getDataGenerator()->create_course();
3452          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
3453  
3454          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [
3455              'userid' => $USER->id,
3456              'course' => $course->id,
3457              'forum' => $forum->id,
3458              'message' => 'Test discussion message',
3459              'messageformat' => FORMAT_HTML,
3460          ]);
3461  
3462          // Update discussion post message & messageformat.
3463          $result = external_api::clean_returnvalue(
3464              mod_forum_external::update_discussion_post_returns(),
3465              mod_forum_external::update_discussion_post($discussion->firstpost, '', 'Update discussion message', FORMAT_MOODLE)
3466          );
3467          $this->assertTrue($result['status']);
3468  
3469          // Get updated discussion post from DB.
3470          $updatedpost = $DB->get_record('forum_posts', ['id' => $discussion->firstpost], 'message,messageformat');
3471          $this->assertEquals((object) [
3472              'message' => 'Update discussion message',
3473              'messageformat' => FORMAT_MOODLE,
3474          ], $updatedpost);
3475      }
3476  }