Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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   * External course functions unit tests
  19   *
  20   * @package    core_course
  21   * @category   external
  22   * @copyright  2012 Jerome Mouneyrac
  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  
  32  /**
  33   * External course functions unit tests
  34   *
  35   * @package    core_course
  36   * @category   external
  37   * @copyright  2012 Jerome Mouneyrac
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class core_course_externallib_testcase extends externallib_advanced_testcase {
  41  
  42      /**
  43       * Tests set up
  44       */
  45      protected function setUp(): void {
  46          global $CFG;
  47          require_once($CFG->dirroot . '/course/externallib.php');
  48      }
  49  
  50      /**
  51       * Test create_categories
  52       */
  53      public function test_create_categories() {
  54  
  55          global $DB;
  56  
  57          $this->resetAfterTest(true);
  58  
  59          // Set the required capabilities by the external function
  60          $contextid = context_system::instance()->id;
  61          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
  62  
  63          // Create base categories.
  64          $category1 = new stdClass();
  65          $category1->name = 'Root Test Category 1';
  66          $category2 = new stdClass();
  67          $category2->name = 'Root Test Category 2';
  68          $category2->idnumber = 'rootcattest2';
  69          $category2->desc = 'Description for root test category 1';
  70          $category2->theme = 'classic';
  71          $categories = array(
  72              array('name' => $category1->name, 'parent' => 0),
  73              array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber,
  74                  'description' => $category2->desc, 'theme' => $category2->theme)
  75          );
  76  
  77          $createdcats = core_course_external::create_categories($categories);
  78  
  79          // We need to execute the return values cleaning process to simulate the web service server.
  80          $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
  81  
  82          // Initially confirm that base data was inserted correctly.
  83          $this->assertEquals($category1->name, $createdcats[0]['name']);
  84          $this->assertEquals($category2->name, $createdcats[1]['name']);
  85  
  86          // Save the ids.
  87          $category1->id = $createdcats[0]['id'];
  88          $category2->id = $createdcats[1]['id'];
  89  
  90          // Create on sub category.
  91          $category3 = new stdClass();
  92          $category3->name = 'Sub Root Test Category 3';
  93          $subcategories = array(
  94              array('name' => $category3->name, 'parent' => $category1->id)
  95          );
  96  
  97          $createdsubcats = core_course_external::create_categories($subcategories);
  98  
  99          // We need to execute the return values cleaning process to simulate the web service server.
 100          $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
 101  
 102          // Confirm that sub categories were inserted correctly.
 103          $this->assertEquals($category3->name, $createdsubcats[0]['name']);
 104  
 105          // Save the ids.
 106          $category3->id = $createdsubcats[0]['id'];
 107  
 108          // Calling the ws function should provide a new sortorder to give category1,
 109          // category2, category3. New course categories are ordered by id not name.
 110          $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
 111          $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
 112          $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
 113  
 114          // sortorder sequence (and sortorder) must be:
 115          // category 1
 116          //   category 3
 117          // category 2
 118          $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
 119          $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
 120  
 121          // Call without required capability
 122          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
 123          $this->expectException('required_capability_exception');
 124          $createdsubcats = core_course_external::create_categories($subcategories);
 125  
 126      }
 127  
 128      /**
 129       * Test delete categories
 130       */
 131      public function test_delete_categories() {
 132          global $DB;
 133  
 134          $this->resetAfterTest(true);
 135  
 136          // Set the required capabilities by the external function
 137          $contextid = context_system::instance()->id;
 138          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
 139  
 140          $category1  = self::getDataGenerator()->create_category();
 141          $category2  = self::getDataGenerator()->create_category(
 142                  array('parent' => $category1->id));
 143          $category3  = self::getDataGenerator()->create_category();
 144          $category4  = self::getDataGenerator()->create_category(
 145                  array('parent' => $category3->id));
 146          $category5  = self::getDataGenerator()->create_category(
 147                  array('parent' => $category4->id));
 148  
 149          //delete category 1 and 2 + delete category 4, category 5 moved under category 3
 150          core_course_external::delete_categories(array(
 151              array('id' => $category1->id, 'recursive' => 1),
 152              array('id' => $category4->id)
 153          ));
 154  
 155          //check $category 1 and 2 are deleted
 156          $notdeletedcount = $DB->count_records_select('course_categories',
 157              'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')');
 158          $this->assertEquals(0, $notdeletedcount);
 159  
 160          //check that $category5 as $category3 for parent
 161          $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id));
 162          $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id);
 163  
 164           // Call without required capability
 165          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
 166          $this->expectException('required_capability_exception');
 167          $createdsubcats = core_course_external::delete_categories(
 168                  array(array('id' => $category3->id)));
 169      }
 170  
 171      /**
 172       * Test get categories
 173       */
 174      public function test_get_categories() {
 175          global $DB;
 176  
 177          $this->resetAfterTest(true);
 178  
 179          $generatedcats = array();
 180          $category1data['idnumber'] = 'idnumbercat1';
 181          $category1data['name'] = 'Category 1 for PHPunit test';
 182          $category1data['description'] = 'Category 1 description';
 183          $category1data['descriptionformat'] = FORMAT_MOODLE;
 184          $category1  = self::getDataGenerator()->create_category($category1data);
 185          $generatedcats[$category1->id] = $category1;
 186          $category2  = self::getDataGenerator()->create_category(
 187                  array('parent' => $category1->id));
 188          $generatedcats[$category2->id] = $category2;
 189          $category6  = self::getDataGenerator()->create_category(
 190                  array('parent' => $category1->id, 'visible' => 0));
 191          $generatedcats[$category6->id] = $category6;
 192          $category3  = self::getDataGenerator()->create_category();
 193          $generatedcats[$category3->id] = $category3;
 194          $category4  = self::getDataGenerator()->create_category(
 195                  array('parent' => $category3->id));
 196          $generatedcats[$category4->id] = $category4;
 197          $category5  = self::getDataGenerator()->create_category(
 198                  array('parent' => $category4->id));
 199          $generatedcats[$category5->id] = $category5;
 200  
 201          // Set the required capabilities by the external function.
 202          $context = context_system::instance();
 203          $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
 204          $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 205  
 206          // Retrieve category1 + sub-categories except not visible ones
 207          $categories = core_course_external::get_categories(array(
 208              array('key' => 'id', 'value' => $category1->id),
 209              array('key' => 'visible', 'value' => 1)), 1);
 210  
 211          // We need to execute the return values cleaning process to simulate the web service server.
 212          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 213  
 214          // Check we retrieve the good total number of categories.
 215          $this->assertEquals(2, count($categories));
 216  
 217          // Check the return values
 218          foreach ($categories as $category) {
 219              $generatedcat = $generatedcats[$category['id']];
 220              $this->assertEquals($category['idnumber'], $generatedcat->idnumber);
 221              $this->assertEquals($category['name'], $generatedcat->name);
 222              // Description was converted to the HTML format.
 223              $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false)));
 224              $this->assertEquals($category['descriptionformat'], FORMAT_HTML);
 225          }
 226  
 227          // Check categories by ids.
 228          $ids = implode(',', array_keys($generatedcats));
 229          $categories = core_course_external::get_categories(array(
 230              array('key' => 'ids', 'value' => $ids)), 0);
 231  
 232          // We need to execute the return values cleaning process to simulate the web service server.
 233          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 234  
 235          // Check we retrieve the good total number of categories.
 236          $this->assertEquals(6, count($categories));
 237          // Check ids.
 238          $returnedids = [];
 239          foreach ($categories as $category) {
 240              $returnedids[] = $category['id'];
 241          }
 242          // Sort the arrays upon comparision.
 243          $this->assertEqualsCanonicalizing(array_keys($generatedcats), $returnedids);
 244  
 245          // Check different params.
 246          $categories = core_course_external::get_categories(array(
 247              array('key' => 'id', 'value' => $category1->id),
 248              array('key' => 'ids', 'value' => $category1->id),
 249              array('key' => 'idnumber', 'value' => $category1->idnumber),
 250              array('key' => 'visible', 'value' => 1)), 0);
 251  
 252          // We need to execute the return values cleaning process to simulate the web service server.
 253          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 254  
 255          $this->assertEquals(1, count($categories));
 256  
 257          // Same query, but forcing a parameters clean.
 258          $categories = core_course_external::get_categories(array(
 259              array('key' => 'id', 'value' => "$category1->id"),
 260              array('key' => 'idnumber', 'value' => $category1->idnumber),
 261              array('key' => 'name', 'value' => $category1->name . "<br/>"),
 262              array('key' => 'visible', 'value' => '1')), 0);
 263          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 264  
 265          $this->assertEquals(1, count($categories));
 266  
 267          // Retrieve categories from parent.
 268          $categories = core_course_external::get_categories(array(
 269              array('key' => 'parent', 'value' => $category3->id)), 1);
 270          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 271  
 272          $this->assertEquals(2, count($categories));
 273  
 274          // Retrieve all categories.
 275          $categories = core_course_external::get_categories();
 276  
 277          // We need to execute the return values cleaning process to simulate the web service server.
 278          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 279  
 280          $this->assertEquals($DB->count_records('course_categories'), count($categories));
 281  
 282          $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 283  
 284          // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
 285          // It should retrieve all visible categories as well.
 286          set_config('maxcategorydepth', 2);
 287          $categories = core_course_external::get_categories();
 288  
 289          // We need to execute the return values cleaning process to simulate the web service server.
 290          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
 291  
 292          $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
 293  
 294          // Call without required capability (it will fail cause of the search on idnumber).
 295          $this->expectException('moodle_exception');
 296          $categories = core_course_external::get_categories(array(
 297              array('key' => 'id', 'value' => $category1->id),
 298              array('key' => 'idnumber', 'value' => $category1->idnumber),
 299              array('key' => 'visible', 'value' => 1)), 0);
 300      }
 301  
 302      /**
 303       * Test update_categories
 304       */
 305      public function test_update_categories() {
 306          global $DB;
 307  
 308          $this->resetAfterTest(true);
 309  
 310          // Set the required capabilities by the external function
 311          $contextid = context_system::instance()->id;
 312          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
 313  
 314          // Create base categories.
 315          $category1data['idnumber'] = 'idnumbercat1';
 316          $category1data['name'] = 'Category 1 for PHPunit test';
 317          $category1data['description'] = 'Category 1 description';
 318          $category1data['descriptionformat'] = FORMAT_MOODLE;
 319          $category1  = self::getDataGenerator()->create_category($category1data);
 320          $category2  = self::getDataGenerator()->create_category(
 321                  array('parent' => $category1->id));
 322          $category3  = self::getDataGenerator()->create_category();
 323          $category4  = self::getDataGenerator()->create_category(
 324                  array('parent' => $category3->id));
 325          $category5  = self::getDataGenerator()->create_category(
 326                  array('parent' => $category4->id));
 327  
 328          // We update all category1 attribut.
 329          // Then we move cat4 and cat5 parent: cat3 => cat1
 330          $categories = array(
 331              array('id' => $category1->id,
 332                  'name' => $category1->name . '_updated',
 333                  'idnumber' => $category1->idnumber . '_updated',
 334                  'description' => $category1->description . '_updated',
 335                  'descriptionformat' => FORMAT_HTML,
 336                  'theme' => $category1->theme),
 337              array('id' => $category4->id, 'parent' => $category1->id));
 338  
 339          core_course_external::update_categories($categories);
 340  
 341          // Check the values were updated.
 342          $dbcategories = $DB->get_records_select('course_categories',
 343                  'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id
 344                  . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')');
 345          $this->assertEquals($category1->name . '_updated',
 346                  $dbcategories[$category1->id]->name);
 347          $this->assertEquals($category1->idnumber . '_updated',
 348                  $dbcategories[$category1->id]->idnumber);
 349          $this->assertEquals($category1->description . '_updated',
 350                  $dbcategories[$category1->id]->description);
 351          $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat);
 352  
 353          // Check that category4 and category5 have been properly moved.
 354          $this->assertEquals('/' . $category1->id . '/' . $category4->id,
 355                  $dbcategories[$category4->id]->path);
 356          $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id,
 357                  $dbcategories[$category5->id]->path);
 358  
 359          // Call without required capability.
 360          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
 361          $this->expectException('required_capability_exception');
 362          core_course_external::update_categories($categories);
 363      }
 364  
 365      /**
 366       * Test update_categories method for moving categories
 367       */
 368      public function test_update_categories_moving() {
 369          $this->resetAfterTest();
 370  
 371          // Create data.
 372          $categorya  = self::getDataGenerator()->create_category([
 373              'name' => 'CAT_A',
 374          ]);
 375          $categoryasub = self::getDataGenerator()->create_category([
 376              'name' => 'SUBCAT_A',
 377              'parent' => $categorya->id
 378          ]);
 379          $categoryb  = self::getDataGenerator()->create_category([
 380              'name' => 'CAT_B',
 381          ]);
 382  
 383          // Create a new test user.
 384          $testuser = self::getDataGenerator()->create_user();
 385          $this->setUser($testuser);
 386  
 387          // Set the capability for CAT_A only.
 388          $contextcata = context_coursecat::instance($categorya->id);
 389          $roleid = $this->assignUserCapability('moodle/category:manage', $contextcata->id);
 390  
 391          // Then we move SUBCAT_A parent: CAT_A => CAT_B.
 392          $categories = [
 393              [
 394                  'id' => $categoryasub->id,
 395                  'parent' => $categoryb->id
 396              ]
 397          ];
 398  
 399          $this->expectException('required_capability_exception');
 400          core_course_external::update_categories($categories);
 401      }
 402  
 403      /**
 404       * Test create_courses numsections
 405       */
 406      public function test_create_course_numsections() {
 407          global $DB;
 408  
 409          $this->resetAfterTest(true);
 410  
 411          // Set the required capabilities by the external function.
 412          $contextid = context_system::instance()->id;
 413          $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
 414          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
 415  
 416          $numsections = 10;
 417          $category  = self::getDataGenerator()->create_category();
 418  
 419          // Create base categories.
 420          $course1['fullname'] = 'Test course 1';
 421          $course1['shortname'] = 'Testcourse1';
 422          $course1['categoryid'] = $category->id;
 423          $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
 424  
 425          $courses = array($course1);
 426  
 427          $createdcourses = core_course_external::create_courses($courses);
 428          foreach ($createdcourses as $createdcourse) {
 429              $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
 430              $modinfo = get_fast_modinfo($createdcourse['id']);
 431              $sections = $modinfo->get_section_info_all();
 432              $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
 433              $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
 434          }
 435      }
 436  
 437      /**
 438       * Test create_courses
 439       */
 440      public function test_create_courses() {
 441          global $DB;
 442  
 443          $this->resetAfterTest(true);
 444  
 445          // Enable course completion.
 446          set_config('enablecompletion', 1);
 447          // Enable course themes.
 448          set_config('allowcoursethemes', 1);
 449  
 450          // Custom fields.
 451          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
 452  
 453          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
 454              'categoryid' => $fieldcategory->get('id'),
 455              'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL]];
 456          $field = self::getDataGenerator()->create_custom_field($customfield);
 457  
 458          // Set the required capabilities by the external function
 459          $contextid = context_system::instance()->id;
 460          $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
 461          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
 462          $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
 463  
 464          $category  = self::getDataGenerator()->create_category();
 465  
 466          // Create base categories.
 467          $course1['fullname'] = 'Test course 1';
 468          $course1['shortname'] = 'Testcourse1';
 469          $course1['categoryid'] = $category->id;
 470          $course2['fullname'] = 'Test course 2';
 471          $course2['shortname'] = 'Testcourse2';
 472          $course2['categoryid'] = $category->id;
 473          $course2['idnumber'] = 'testcourse2idnumber';
 474          $course2['summary'] = 'Description for course 2';
 475          $course2['summaryformat'] = FORMAT_MOODLE;
 476          $course2['format'] = 'weeks';
 477          $course2['showgrades'] = 1;
 478          $course2['newsitems'] = 3;
 479          $course2['startdate'] = 1420092000; // 01/01/2015.
 480          $course2['enddate'] = 1422669600; // 01/31/2015.
 481          $course2['numsections'] = 4;
 482          $course2['maxbytes'] = 100000;
 483          $course2['showreports'] = 1;
 484          $course2['visible'] = 0;
 485          $course2['hiddensections'] = 0;
 486          $course2['groupmode'] = 0;
 487          $course2['groupmodeforce'] = 0;
 488          $course2['defaultgroupingid'] = 0;
 489          $course2['enablecompletion'] = 1;
 490          $course2['completionnotify'] = 1;
 491          $course2['lang'] = 'en';
 492          $course2['forcetheme'] = 'classic';
 493          $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
 494          $course3['fullname'] = 'Test course 3';
 495          $course3['shortname'] = 'Testcourse3';
 496          $course3['categoryid'] = $category->id;
 497          $course3['format'] = 'topics';
 498          $course3options = array('numsections' => 8,
 499              'hiddensections' => 1,
 500              'coursedisplay' => 1);
 501          $course3['courseformatoptions'] = array();
 502          foreach ($course3options as $key => $value) {
 503              $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
 504          }
 505          $course4['fullname'] = 'Test course with custom fields';
 506          $course4['shortname'] = 'Testcoursecustomfields';
 507          $course4['categoryid'] = $category->id;
 508          $course4['customfields'] = [['shortname' => $customfield['shortname'], 'value' => 'Test value']];
 509          $courses = array($course4, $course1, $course2, $course3);
 510  
 511          $createdcourses = core_course_external::create_courses($courses);
 512  
 513          // We need to execute the return values cleaning process to simulate the web service server.
 514          $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
 515  
 516          // Check that right number of courses were created.
 517          $this->assertEquals(4, count($createdcourses));
 518  
 519          // Check that the courses were correctly created.
 520          foreach ($createdcourses as $createdcourse) {
 521              $courseinfo = course_get_format($createdcourse['id'])->get_course();
 522  
 523              if ($createdcourse['shortname'] == $course2['shortname']) {
 524                  $this->assertEquals($courseinfo->fullname, $course2['fullname']);
 525                  $this->assertEquals($courseinfo->shortname, $course2['shortname']);
 526                  $this->assertEquals($courseinfo->category, $course2['categoryid']);
 527                  $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
 528                  $this->assertEquals($courseinfo->summary, $course2['summary']);
 529                  $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
 530                  $this->assertEquals($courseinfo->format, $course2['format']);
 531                  $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
 532                  $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
 533                  $this->assertEquals($courseinfo->startdate, $course2['startdate']);
 534                  $this->assertEquals($courseinfo->enddate, $course2['enddate']);
 535                  $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
 536                  $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
 537                  $this->assertEquals($courseinfo->showreports, $course2['showreports']);
 538                  $this->assertEquals($courseinfo->visible, $course2['visible']);
 539                  $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
 540                  $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
 541                  $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
 542                  $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
 543                  $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
 544                  $this->assertEquals($courseinfo->lang, $course2['lang']);
 545                  $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
 546  
 547                  // We enabled completion at the beginning of the test.
 548                  $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
 549  
 550              } else if ($createdcourse['shortname'] == $course1['shortname']) {
 551                  $courseconfig = get_config('moodlecourse');
 552                  $this->assertEquals($courseinfo->fullname, $course1['fullname']);
 553                  $this->assertEquals($courseinfo->shortname, $course1['shortname']);
 554                  $this->assertEquals($courseinfo->category, $course1['categoryid']);
 555                  $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
 556                  $this->assertEquals($courseinfo->format, $courseconfig->format);
 557                  $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
 558                  $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
 559                  $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
 560                  $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
 561                  $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
 562                  $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
 563                  $this->assertEquals($courseinfo->defaultgroupingid, 0);
 564              } else if ($createdcourse['shortname'] == $course3['shortname']) {
 565                  $this->assertEquals($courseinfo->fullname, $course3['fullname']);
 566                  $this->assertEquals($courseinfo->shortname, $course3['shortname']);
 567                  $this->assertEquals($courseinfo->category, $course3['categoryid']);
 568                  $this->assertEquals($courseinfo->format, $course3['format']);
 569                  $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
 570                  $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
 571                      $course3options['numsections']);
 572                  $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
 573              } else if ($createdcourse['shortname'] == $course4['shortname']) {
 574                  $this->assertEquals($courseinfo->fullname, $course4['fullname']);
 575                  $this->assertEquals($courseinfo->shortname, $course4['shortname']);
 576                  $this->assertEquals($courseinfo->category, $course4['categoryid']);
 577  
 578                  $handler = core_course\customfield\course_handler::create();
 579                  $customfields = $handler->export_instance_data_object($createdcourse['id']);
 580                  $this->assertEquals((object)['test' => 'Test value'], $customfields);
 581              } else {
 582                  throw new moodle_exception('Unexpected shortname');
 583              }
 584          }
 585  
 586          // Call without required capability
 587          $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
 588          $this->expectException('required_capability_exception');
 589          $createdsubcats = core_course_external::create_courses($courses);
 590      }
 591  
 592      /**
 593       * Data provider for testing empty fields produce expected exceptions
 594       *
 595       * @see test_create_courses_empty_field
 596       * @see test_update_courses_empty_field
 597       *
 598       * @return array
 599       */
 600      public function course_empty_field_provider(): array {
 601          return [
 602              [[
 603                  'fullname' => '',
 604                  'shortname' => 'ws101',
 605              ], 'fullname'],
 606              [[
 607                  'fullname' => ' ',
 608                  'shortname' => 'ws101',
 609              ], 'fullname'],
 610              [[
 611                  'fullname' => 'Web Services',
 612                  'shortname' => '',
 613              ], 'shortname'],
 614              [[
 615                  'fullname' => 'Web Services',
 616                  'shortname' => ' ',
 617              ], 'shortname'],
 618          ];
 619      }
 620  
 621      /**
 622       * Test creating courses with empty fields throws an exception
 623       *
 624       * @param array $course
 625       * @param string $expectedemptyfield
 626       *
 627       * @dataProvider course_empty_field_provider
 628       */
 629      public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
 630          $this->resetAfterTest();
 631          $this->setAdminUser();
 632  
 633          // Create a category for the new course.
 634          $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
 635  
 636          $this->expectException(moodle_exception::class);
 637          $this->expectExceptionMessageMatches("/{$expectedemptyfield}/");
 638          core_course_external::create_courses([$course]);
 639      }
 640  
 641      /**
 642       * Test updating courses with empty fields returns warnings
 643       *
 644       * @param array $course
 645       * @param string $expectedemptyfield
 646       *
 647       * @dataProvider course_empty_field_provider
 648       */
 649      public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
 650          $this->resetAfterTest();
 651          $this->setAdminUser();
 652  
 653          // Create a course to update.
 654          $course['id'] = $this->getDataGenerator()->create_course()->id;
 655  
 656          $result = core_course_external::update_courses([$course]);
 657          $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
 658  
 659          $this->assertCount(1, $result['warnings']);
 660  
 661          $warning = reset($result['warnings']);
 662          $this->assertEquals('errorinvalidparam', $warning['warningcode']);
 663          $this->assertStringContainsString($expectedemptyfield, $warning['message']);
 664      }
 665  
 666      /**
 667       * Test delete_courses
 668       */
 669      public function test_delete_courses() {
 670          global $DB, $USER;
 671  
 672          $this->resetAfterTest(true);
 673  
 674          // Admin can delete a course.
 675          $this->setAdminUser();
 676          // Validate_context() will fail as the email is not set by $this->setAdminUser().
 677          $USER->email = 'emailtopass@example.com';
 678  
 679          $course1  = self::getDataGenerator()->create_course();
 680          $course2  = self::getDataGenerator()->create_course();
 681          $course3  = self::getDataGenerator()->create_course();
 682  
 683          // Delete courses.
 684          $result = core_course_external::delete_courses(array($course1->id, $course2->id));
 685          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 686          // Check for 0 warnings.
 687          $this->assertEquals(0, count($result['warnings']));
 688  
 689          // Check $course 1 and 2 are deleted.
 690          $notdeletedcount = $DB->count_records_select('course',
 691              'id IN ( ' . $course1->id . ',' . $course2->id . ')');
 692          $this->assertEquals(0, $notdeletedcount);
 693  
 694          // Try to delete non-existent course.
 695          $result = core_course_external::delete_courses(array($course1->id));
 696          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 697          // Check for 1 warnings.
 698          $this->assertEquals(1, count($result['warnings']));
 699  
 700          // Try to delete Frontpage course.
 701          $result = core_course_external::delete_courses(array(0));
 702          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 703          // Check for 1 warnings.
 704          $this->assertEquals(1, count($result['warnings']));
 705  
 706           // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
 707          $student1 = self::getDataGenerator()->create_user();
 708          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 709          $this->getDataGenerator()->enrol_user($student1->id,
 710                                                $course3->id,
 711                                                $studentrole->id);
 712          $this->setUser($student1);
 713          $result = core_course_external::delete_courses(array($course3->id));
 714          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 715          // Check for 1 warnings.
 716          $this->assertEquals(1, count($result['warnings']));
 717  
 718           // Fail when the user is not allow to access the course (enrolled) or is not admin.
 719          $this->setGuestUser();
 720          $this->expectException('require_login_exception');
 721  
 722          $result = core_course_external::delete_courses(array($course3->id));
 723          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
 724      }
 725  
 726      /**
 727       * Test get_courses
 728       */
 729      public function test_get_courses () {
 730          global $DB;
 731  
 732          $this->resetAfterTest(true);
 733  
 734          $generatedcourses = array();
 735          $coursedata['idnumber'] = 'idnumbercourse1';
 736          // Adding tags here to check that format_string is applied.
 737          $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
 738          $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
 739          $coursedata['summary'] = 'Course 1 description';
 740          $coursedata['summaryformat'] = FORMAT_MOODLE;
 741          $course1  = self::getDataGenerator()->create_course($coursedata);
 742  
 743          $fieldcategory = self::getDataGenerator()->create_custom_field_category(
 744              ['name' => 'Other fields']);
 745  
 746          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
 747              'categoryid' => $fieldcategory->get('id')];
 748          $field = self::getDataGenerator()->create_custom_field($customfield);
 749  
 750          $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
 751  
 752          $generatedcourses[$course1->id] = $course1;
 753          $course2  = self::getDataGenerator()->create_course();
 754          $generatedcourses[$course2->id] = $course2;
 755          $course3  = self::getDataGenerator()->create_course(array('format' => 'topics'));
 756          $generatedcourses[$course3->id] = $course3;
 757          $course4  = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]);
 758          $generatedcourses[$course4->id] = $course4;
 759  
 760          // Set the required capabilities by the external function.
 761          $context = context_system::instance();
 762          $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
 763          $this->assignUserCapability('moodle/course:update',
 764                  context_course::instance($course1->id)->id, $roleid);
 765          $this->assignUserCapability('moodle/course:update',
 766                  context_course::instance($course2->id)->id, $roleid);
 767          $this->assignUserCapability('moodle/course:update',
 768                  context_course::instance($course3->id)->id, $roleid);
 769          $this->assignUserCapability('moodle/course:update',
 770                  context_course::instance($course4->id)->id, $roleid);
 771  
 772          $courses = core_course_external::get_courses(array('ids' =>
 773              array($course1->id, $course2->id, $course4->id)));
 774  
 775          // We need to execute the return values cleaning process to simulate the web service server.
 776          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
 777  
 778          // Check we retrieve the good total number of courses.
 779          $this->assertEquals(3, count($courses));
 780  
 781          foreach ($courses as $course) {
 782              $coursecontext = context_course::instance($course['id']);
 783              $dbcourse = $generatedcourses[$course['id']];
 784              $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
 785              $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id));
 786              $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse),
 787                  $coursecontext->id));
 788              // Summary was converted to the HTML format.
 789              $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
 790              $this->assertEquals($course['summaryformat'], FORMAT_HTML);
 791              $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id));
 792              $this->assertEquals($course['categoryid'], $dbcourse->category);
 793              $this->assertEquals($course['format'], $dbcourse->format);
 794              $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
 795              $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
 796              $this->assertEquals($course['startdate'], $dbcourse->startdate);
 797              $this->assertEquals($course['enddate'], $dbcourse->enddate);
 798              $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
 799              $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
 800              $this->assertEquals($course['showreports'], $dbcourse->showreports);
 801              $this->assertEquals($course['visible'], $dbcourse->visible);
 802              $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
 803              $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
 804              $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
 805              $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
 806              $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
 807              $this->assertEquals($course['lang'], $dbcourse->lang);
 808              $this->assertEquals($course['forcetheme'], $dbcourse->theme);
 809              $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
 810              if ($dbcourse->format === 'topics') {
 811                  $this->assertEquals($course['courseformatoptions'], array(
 812                      array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
 813                      array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
 814                  ));
 815              }
 816  
 817              // Assert custom field that we previously added to test course 4.
 818              if ($dbcourse->id == $course4->id) {
 819                  $this->assertEquals([
 820                      'shortname' => $customfield['shortname'],
 821                      'name' => $customfield['name'],
 822                      'type' => $customfield['type'],
 823                      'value' => $customfieldvalue['value'],
 824                      'valueraw' => $customfieldvalue['value'],
 825                  ], $course['customfields'][0]);
 826              }
 827          }
 828  
 829          // Get all courses in the DB
 830          $courses = core_course_external::get_courses(array());
 831  
 832          // We need to execute the return values cleaning process to simulate the web service server.
 833          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
 834  
 835          $this->assertEquals($DB->count_records('course'), count($courses));
 836      }
 837  
 838      /**
 839       * Test retrieving courses returns custom field data
 840       */
 841      public function test_get_courses_customfields(): void {
 842          $this->resetAfterTest();
 843          $this->setAdminUser();
 844  
 845          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
 846          $datefield = $this->getDataGenerator()->create_custom_field([
 847              'categoryid' => $fieldcategory->get('id'),
 848              'shortname' => 'mydate',
 849              'name' => 'My date',
 850              'type' => 'date',
 851          ]);
 852  
 853          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
 854              [
 855                  'shortname' => $datefield->get('shortname'),
 856                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
 857              ],
 858          ]]);
 859  
 860          $courses = external_api::clean_returnvalue(
 861              core_course_external::get_courses_returns(),
 862              core_course_external::get_courses(['ids' => [$newcourse->id]])
 863          );
 864  
 865          $this->assertCount(1, $courses);
 866          $course = reset($courses);
 867  
 868          $this->assertArrayHasKey('customfields', $course);
 869          $this->assertCount(1, $course['customfields']);
 870  
 871          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
 872          $this->assertEquals([
 873              'name' => $datefield->get('name'),
 874              'shortname' => $datefield->get('shortname'),
 875              'type' => $datefield->get('type'),
 876              'value' => userdate(1580389200),
 877              'valueraw' => 1580389200,
 878          ], reset($course['customfields']));
 879      }
 880  
 881      /**
 882       * Test get_courses without capability
 883       */
 884      public function test_get_courses_without_capability() {
 885          $this->resetAfterTest(true);
 886  
 887          $course1 = $this->getDataGenerator()->create_course();
 888          $this->setUser($this->getDataGenerator()->create_user());
 889  
 890          // No permissions are required to get the site course.
 891          $courses = core_course_external::get_courses(array('ids' => [SITEID]));
 892          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
 893  
 894          $this->assertEquals(1, count($courses));
 895          $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
 896          $this->assertEquals('site', $courses[0]['format']);
 897  
 898          // Requesting course without being enrolled or capability to view it will throw an exception.
 899          try {
 900              core_course_external::get_courses(array('ids' => [$course1->id]));
 901              $this->fail('Exception expected');
 902          } catch (moodle_exception $e) {
 903              $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
 904          }
 905      }
 906  
 907      /**
 908       * Test search_courses
 909       */
 910      public function test_search_courses () {
 911  
 912          global $DB;
 913  
 914          $this->resetAfterTest(true);
 915          $this->setAdminUser();
 916          $generatedcourses = array();
 917          $coursedata1['fullname'] = 'FIRST COURSE';
 918          $course1  = self::getDataGenerator()->create_course($coursedata1);
 919  
 920          $page = new moodle_page();
 921          $page->set_course($course1);
 922          $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
 923  
 924          $coursedata2['fullname'] = 'SECOND COURSE';
 925          $course2  = self::getDataGenerator()->create_course($coursedata2);
 926  
 927          $page = new moodle_page();
 928          $page->set_course($course2);
 929          $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
 930  
 931          // Search by name.
 932          $results = core_course_external::search_courses('search', 'FIRST');
 933          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 934          $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
 935          $this->assertCount(1, $results['courses']);
 936  
 937          // Create the forum.
 938          $record = new stdClass();
 939          $record->introformat = FORMAT_HTML;
 940          $record->course = $course2->id;
 941          // Set Aggregate type = Average of ratings.
 942          $forum = self::getDataGenerator()->create_module('forum', $record);
 943  
 944          // Search by module.
 945          $results = core_course_external::search_courses('modulelist', 'forum');
 946          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 947          $this->assertEquals(1, $results['total']);
 948  
 949          // Enable coursetag option.
 950          set_config('block_tags_showcoursetags', true);
 951          // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
 952          core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
 953                  array('TAG-LABEL ON SECOND COURSE'));
 954          $taginstance = $DB->get_record('tag_instance',
 955                  array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
 956  
 957          // Search by tagid.
 958          $results = core_course_external::search_courses('tagid', $taginstance->tagid);
 959          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 960          $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
 961  
 962          // Search by block (use news_items default block).
 963          $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
 964          $results = core_course_external::search_courses('blocklist', $blockid);
 965          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 966          $this->assertEquals(2, $results['total']);
 967  
 968          // Now as a normal user.
 969          $user = self::getDataGenerator()->create_user();
 970  
 971          // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
 972          $coursedata3['fullname'] = 'HIDDEN COURSE';
 973          $coursedata3['visible'] = 0;
 974          $course3  = self::getDataGenerator()->create_course($coursedata3);
 975          $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
 976  
 977          $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
 978          $this->setUser($user);
 979  
 980          $results = core_course_external::search_courses('search', 'FIRST');
 981          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 982          $this->assertCount(1, $results['courses']);
 983          $this->assertEquals(1, $results['total']);
 984          $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
 985  
 986          // Check that we can see all courses without the limit to enrolled setting.
 987          $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
 988          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 989          $this->assertCount(2, $results['courses']);
 990          $this->assertEquals(2, $results['total']);
 991  
 992          // Check that we only see our enrolled course when limiting.
 993          $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
 994          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
 995          $this->assertCount(1, $results['courses']);
 996          $this->assertEquals(1, $results['total']);
 997          $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
 998  
 999          // Search by block (use news_items default block). Should fail (only admins allowed).
1000          $this->expectException('required_capability_exception');
1001          $results = core_course_external::search_courses('blocklist', $blockid);
1002      }
1003  
1004      /**
1005       * Test searching for courses returns custom field data
1006       */
1007      public function test_search_courses_customfields(): void {
1008          $this->resetAfterTest();
1009          $this->setAdminUser();
1010  
1011          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
1012          $datefield = $this->getDataGenerator()->create_custom_field([
1013              'categoryid' => $fieldcategory->get('id'),
1014              'shortname' => 'mydate',
1015              'name' => 'My date',
1016              'type' => 'date',
1017          ]);
1018  
1019          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
1020              [
1021                  'shortname' => $datefield->get('shortname'),
1022                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
1023              ],
1024          ]]);
1025  
1026          $result = external_api::clean_returnvalue(
1027              core_course_external::search_courses_returns(),
1028              core_course_external::search_courses('search', $newcourse->shortname)
1029          );
1030  
1031          $this->assertCount(1, $result['courses']);
1032          $course = reset($result['courses']);
1033  
1034          $this->assertArrayHasKey('customfields', $course);
1035          $this->assertCount(1, $course['customfields']);
1036  
1037          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
1038          $this->assertEquals([
1039              'name' => $datefield->get('name'),
1040              'shortname' => $datefield->get('shortname'),
1041              'type' => $datefield->get('type'),
1042              'value' => userdate(1580389200),
1043              'valueraw' => 1580389200,
1044          ], reset($course['customfields']));
1045      }
1046  
1047      /**
1048       * Create a course with contents
1049       * @return array A list with the course object and course modules objects
1050       */
1051      private function prepare_get_course_contents_test() {
1052          global $DB, $CFG;
1053  
1054          $CFG->allowstealth = 1; // Allow stealth activities.
1055          $CFG->enablecompletion = true;
1056          // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
1057          $course  = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
1058  
1059          $forumdescription = 'This is the forum description';
1060          $forum = $this->getDataGenerator()->create_module('forum',
1061              array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
1062              array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
1063          $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1064          // Add discussions to the tracking forced forum.
1065          $record = new stdClass();
1066          $record->course = $course->id;
1067          $record->userid = 0;
1068          $record->forum = $forum->id;
1069          $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1070          $data = $this->getDataGenerator()->create_module('data',
1071              array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
1072          $datacm = get_coursemodule_from_instance('data', $data->id);
1073          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1074          $pagecm = get_coursemodule_from_instance('page', $page->id);
1075          // This is an stealth page (set by visibleoncoursepage).
1076          $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
1077          $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
1078                  So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
1079          $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
1080              'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
1081          $labelcm = get_coursemodule_from_instance('label', $label->id);
1082          $tomorrow = time() + DAYSECS;
1083          // Module with availability restrictions not met.
1084          $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
1085                  .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
1086          $url = $this->getDataGenerator()->create_module('url',
1087              array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
1088                  'popupwidth' => 100, 'popupheight' => 100),
1089              array('availability' => $availability));
1090          $urlcm = get_coursemodule_from_instance('url', $url->id);
1091          // Module for the last section.
1092          $this->getDataGenerator()->create_module('url',
1093              array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
1094          // Module for section 1 with availability restrictions met.
1095          $yesterday = time() - DAYSECS;
1096          $this->getDataGenerator()->create_module('url',
1097              array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
1098              array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
1099  
1100          // Set the required capabilities by the external function.
1101          $context = context_course::instance($course->id);
1102          $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
1103          $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
1104          $this->assignUserCapability('mod/data:view', $context->id, $roleid);
1105  
1106          $conditions = array('course' => $course->id, 'section' => 2);
1107          $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
1108  
1109          // Add date availability condition not met for section 3.
1110          $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
1111          $DB->set_field('course_sections', 'availability', $availability,
1112                  array('course' => $course->id, 'section' => 3));
1113  
1114          // Create resource for last section.
1115          $pageinhiddensection = $this->getDataGenerator()->create_module('page',
1116              array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
1117          // Set not visible last section.
1118          $DB->set_field('course_sections', 'visible', 0,
1119                  array('course' => $course->id, 'section' => 4));
1120  
1121          $forumcompleteauto = $this->getDataGenerator()->create_module('forum',
1122              array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
1123              array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
1124          $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
1125  
1126          rebuild_course_cache($course->id, true);
1127  
1128          return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
1129      }
1130  
1131      /**
1132       * Test get_course_contents
1133       */
1134      public function test_get_course_contents() {
1135          global $CFG;
1136          $this->resetAfterTest(true);
1137  
1138          $CFG->forum_allowforcedreadtracking = 1;
1139          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1140  
1141          // We first run the test as admin.
1142          $this->setAdminUser();
1143          $sections = core_course_external::get_course_contents($course->id, array());
1144          // We need to execute the return values cleaning process to simulate the web service server.
1145          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1146  
1147          $modinfo = get_fast_modinfo($course);
1148          $testexecuted = 0;
1149          foreach ($sections[0]['modules'] as $module) {
1150              if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
1151                  $cm = $modinfo->cms[$forumcm->id];
1152                  $formattedtext = format_text($cm->content, FORMAT_HTML,
1153                      array('noclean' => true, 'para' => false, 'filter' => false));
1154                  $this->assertEquals($formattedtext, $module['description']);
1155                  $this->assertEquals($forumcm->instance, $module['instance']);
1156                  $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
1157                  $this->assertStringContainsString('1 unread post', $module['afterlink']);
1158                  $this->assertFalse($module['noviewlink']);
1159                  $this->assertNotEmpty($module['description']);  // Module showdescription is on.
1160                  $testexecuted = $testexecuted + 2;
1161              } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
1162                  $cm = $modinfo->cms[$labelcm->id];
1163                  $formattedtext = format_text($cm->content, FORMAT_HTML,
1164                      array('noclean' => true, 'para' => false, 'filter' => false));
1165                  $this->assertEquals($formattedtext, $module['description']);
1166                  $this->assertEquals($labelcm->instance, $module['instance']);
1167                  $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
1168                  $this->assertTrue($module['noviewlink']);
1169                  $this->assertNotEmpty($module['description']);  // Label always prints the description.
1170                  $testexecuted = $testexecuted + 1;
1171              } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
1172                  $this->assertStringContainsString('customcompletionrules', $module['customdata']);
1173                  $this->assertFalse($module['noviewlink']);
1174                  $this->assertArrayNotHasKey('description', $module);
1175                  $testexecuted = $testexecuted + 1;
1176              }
1177          }
1178          foreach ($sections[2]['modules'] as $module) {
1179              if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
1180                  $this->assertStringContainsString('width=100,height=100', $module['onclick']);
1181                  $testexecuted = $testexecuted + 1;
1182              }
1183          }
1184  
1185          $CFG->forum_allowforcedreadtracking = 0;    // Recover original value.
1186          forum_tp_count_forum_unread_posts($forumcm, $course, true);    // Reset static cache for further tests.
1187  
1188          $this->assertEquals(5, $testexecuted);
1189          $this->assertEquals(0, $sections[0]['section']);
1190  
1191          $this->assertCount(6, $sections[0]['modules']);
1192          $this->assertCount(1, $sections[1]['modules']);
1193          $this->assertCount(1, $sections[2]['modules']);
1194          $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
1195          $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
1196          $this->assertNotEmpty($sections[3]['availabilityinfo']);
1197          $this->assertEquals(1, $sections[1]['section']);
1198          $this->assertEquals(2, $sections[2]['section']);
1199          $this->assertEquals(3, $sections[3]['section']);
1200          $this->assertEquals(4, $sections[4]['section']);
1201          $this->assertStringContainsString('<iframe', $sections[2]['summary']);
1202          $this->assertStringContainsString('</iframe>', $sections[2]['summary']);
1203          $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
1204          try {
1205              $sections = core_course_external::get_course_contents($course->id,
1206                                                                      array(array("name" => "invalid", "value" => 1)));
1207              $this->fail('Exception expected due to invalid option.');
1208          } catch (moodle_exception $e) {
1209              $this->assertEquals('errorinvalidparam', $e->errorcode);
1210          }
1211      }
1212  
1213  
1214      /**
1215       * Test get_course_contents as student
1216       */
1217      public function test_get_course_contents_student() {
1218          global $DB;
1219          $this->resetAfterTest(true);
1220  
1221          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1222  
1223          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1224          $user = self::getDataGenerator()->create_user();
1225          self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1226          $this->setUser($user);
1227  
1228          $sections = core_course_external::get_course_contents($course->id, array());
1229          // We need to execute the return values cleaning process to simulate the web service server.
1230          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1231  
1232          $this->assertCount(4, $sections); // Nothing for the not visible section.
1233          $this->assertCount(6, $sections[0]['modules']);
1234          $this->assertCount(1, $sections[1]['modules']);
1235          $this->assertCount(1, $sections[2]['modules']);
1236          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1237  
1238          $this->assertNotEmpty($sections[3]['availabilityinfo']);
1239          $this->assertEquals(1, $sections[1]['section']);
1240          $this->assertEquals(2, $sections[2]['section']);
1241          $this->assertEquals(3, $sections[3]['section']);
1242          // The module with the availability restriction met is returning contents.
1243          $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1244          // The module with the availability restriction not met is not returning contents.
1245          $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1246  
1247          // Now include flag for returning stealth information (fake section).
1248          $sections = core_course_external::get_course_contents($course->id,
1249              array(array("name" => "includestealthmodules", "value" => 1)));
1250          // We need to execute the return values cleaning process to simulate the web service server.
1251          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1252  
1253          $this->assertCount(5, $sections); // Include fake section with stealth activities.
1254          $this->assertCount(6, $sections[0]['modules']);
1255          $this->assertCount(1, $sections[1]['modules']);
1256          $this->assertCount(1, $sections[2]['modules']);
1257          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1258          $this->assertCount(1, $sections[4]['modules']); // One stealth module.
1259          $this->assertEquals(-1, $sections[4]['id']);
1260      }
1261  
1262      /**
1263       * Test get_course_contents excluding modules
1264       */
1265      public function test_get_course_contents_excluding_modules() {
1266          $this->resetAfterTest(true);
1267  
1268          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1269  
1270          // Test exclude modules.
1271          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1272  
1273          // We need to execute the return values cleaning process to simulate the web service server.
1274          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1275  
1276          $this->assertEmpty($sections[0]['modules']);
1277          $this->assertEmpty($sections[1]['modules']);
1278      }
1279  
1280      /**
1281       * Test get_course_contents excluding contents
1282       */
1283      public function test_get_course_contents_excluding_contents() {
1284          $this->resetAfterTest(true);
1285  
1286          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1287  
1288          // Test exclude modules.
1289          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1290  
1291          // We need to execute the return values cleaning process to simulate the web service server.
1292          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1293  
1294          foreach ($sections as $section) {
1295              foreach ($section['modules'] as $module) {
1296                  // Only resources return contents.
1297                  if (isset($module['contents'])) {
1298                      $this->assertEmpty($module['contents']);
1299                  }
1300              }
1301          }
1302      }
1303  
1304      /**
1305       * Test get_course_contents filtering by section number
1306       */
1307      public function test_get_course_contents_section_number() {
1308          $this->resetAfterTest(true);
1309  
1310          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1311  
1312          // Test exclude modules.
1313          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1314  
1315          // We need to execute the return values cleaning process to simulate the web service server.
1316          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1317  
1318          $this->assertCount(1, $sections);
1319          $this->assertCount(6, $sections[0]['modules']);
1320      }
1321  
1322      /**
1323       * Test get_course_contents filtering by cmid
1324       */
1325      public function test_get_course_contents_cmid() {
1326          $this->resetAfterTest(true);
1327  
1328          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1329  
1330          // Test exclude modules.
1331          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1332  
1333          // We need to execute the return values cleaning process to simulate the web service server.
1334          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1335  
1336          $this->assertCount(4, $sections);
1337          $this->assertCount(1, $sections[0]['modules']);
1338          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1339      }
1340  
1341  
1342      /**
1343       * Test get_course_contents filtering by cmid and section
1344       */
1345      public function test_get_course_contents_section_cmid() {
1346          $this->resetAfterTest(true);
1347  
1348          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1349  
1350          // Test exclude modules.
1351          $sections = core_course_external::get_course_contents($course->id, array(
1352                                                                          array("name" => "cmid", "value" => $forumcm->id),
1353                                                                          array("name" => "sectionnumber", "value" => 0)
1354                                                                          ));
1355  
1356          // We need to execute the return values cleaning process to simulate the web service server.
1357          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1358  
1359          $this->assertCount(1, $sections);
1360          $this->assertCount(1, $sections[0]['modules']);
1361          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1362      }
1363  
1364      /**
1365       * Test get_course_contents filtering by modname
1366       */
1367      public function test_get_course_contents_modname() {
1368          $this->resetAfterTest(true);
1369  
1370          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1371  
1372          // Test exclude modules.
1373          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1374  
1375          // We need to execute the return values cleaning process to simulate the web service server.
1376          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1377  
1378          $this->assertCount(4, $sections);
1379          $this->assertCount(2, $sections[0]['modules']);
1380          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1381      }
1382  
1383      /**
1384       * Test get_course_contents filtering by modname
1385       */
1386      public function test_get_course_contents_modid() {
1387          $this->resetAfterTest(true);
1388  
1389          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1390  
1391          // Test exclude modules.
1392          $sections = core_course_external::get_course_contents($course->id, array(
1393                                                                              array("name" => "modname", "value" => "page"),
1394                                                                              array("name" => "modid", "value" => $pagecm->instance),
1395                                                                              ));
1396  
1397          // We need to execute the return values cleaning process to simulate the web service server.
1398          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1399  
1400          $this->assertCount(4, $sections);
1401          $this->assertCount(1, $sections[0]['modules']);
1402          $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1403          $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1404      }
1405  
1406      /**
1407       * Test get course contents completion manual
1408       */
1409      public function test_get_course_contents_completion_manual() {
1410          global $CFG;
1411          $this->resetAfterTest(true);
1412  
1413          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1414              $this->prepare_get_course_contents_test();
1415          availability_completion\condition::wipe_static_cache();
1416  
1417          // Test activity not completed yet.
1418          $result = core_course_external::get_course_contents($course->id, array(
1419              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1420          // We need to execute the return values cleaning process to simulate the web service server.
1421          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1422  
1423          $completiondata = $result[0]['modules'][0]["completiondata"];
1424          $this->assertCount(1, $result[0]['modules']);
1425          $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1426          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1427          $this->assertEquals(0, $completiondata['state']);
1428          $this->assertEquals(0, $completiondata['timecompleted']);
1429          $this->assertEmpty($completiondata['overrideby']);
1430          $this->assertFalse($completiondata['valueused']);
1431          $this->assertTrue($completiondata['hascompletion']);
1432          $this->assertFalse($completiondata['isautomatic']);
1433          $this->assertFalse($completiondata['istrackeduser']);
1434          $this->assertTrue($completiondata['uservisible']);
1435  
1436          // Set activity completed.
1437          core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1438  
1439          $result = core_course_external::get_course_contents($course->id, array(
1440              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1441          // We need to execute the return values cleaning process to simulate the web service server.
1442          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1443  
1444          $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1445          $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1446          $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1447  
1448          // Test activity with completion value that is used in an availability condition.
1449          $result = core_course_external::get_course_contents($course->id, array(
1450                  array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
1451          // We need to execute the return values cleaning process to simulate the web service server.
1452          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1453  
1454          $completiondata = $result[0]['modules'][0]["completiondata"];
1455          $this->assertCount(1, $result[0]['modules']);
1456          $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
1457          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1458          $this->assertEquals(0, $completiondata['state']);
1459          $this->assertEquals(0, $completiondata['timecompleted']);
1460          $this->assertEmpty($completiondata['overrideby']);
1461          $this->assertTrue($completiondata['valueused']);
1462          $this->assertTrue($completiondata['hascompletion']);
1463          $this->assertFalse($completiondata['isautomatic']);
1464          $this->assertFalse($completiondata['istrackeduser']);
1465          $this->assertTrue($completiondata['uservisible']);
1466  
1467          // Disable completion.
1468          $CFG->enablecompletion = 0;
1469          $result = core_course_external::get_course_contents($course->id, array(
1470              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1471          // We need to execute the return values cleaning process to simulate the web service server.
1472          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1473  
1474          $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1475      }
1476  
1477      /**
1478       * Test get course contents completion auto
1479       */
1480      public function test_get_course_contents_completion_auto() {
1481          global $CFG;
1482          $this->resetAfterTest(true);
1483  
1484          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1485              $this->prepare_get_course_contents_test();
1486          availability_completion\condition::wipe_static_cache();
1487  
1488          // Test activity not completed yet.
1489          $result = core_course_external::get_course_contents($course->id, [
1490              [
1491                  "name" => "modname",
1492                  "value" => "forum"
1493              ],
1494              [
1495                  "name" => "modid",
1496                  "value" => $forumcompleteautocm->instance
1497              ]
1498          ]);
1499          // We need to execute the return values cleaning process to simulate the web service server.
1500          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1501  
1502          $forummod = $result[0]['modules'][0];
1503          $completiondata = $forummod["completiondata"];
1504          $this->assertCount(1, $result[0]['modules']);
1505          $this->assertEquals("forum", $forummod["modname"]);
1506          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]);
1507          $this->assertEquals(0, $completiondata['state']);
1508          $this->assertEquals(0, $completiondata['timecompleted']);
1509          $this->assertEmpty($completiondata['overrideby']);
1510          $this->assertFalse($completiondata['valueused']);
1511          $this->assertTrue($completiondata['hascompletion']);
1512          $this->assertTrue($completiondata['isautomatic']);
1513          $this->assertFalse($completiondata['istrackeduser']);
1514          $this->assertTrue($completiondata['uservisible']);
1515          $this->assertCount(1, $completiondata['details']);
1516      }
1517  
1518      /**
1519       * Test mimetype is returned for resources with showtype set.
1520       */
1521      public function test_get_course_contents_including_mimetype() {
1522          $this->resetAfterTest(true);
1523  
1524          $this->setAdminUser();
1525          $course = self::getDataGenerator()->create_course();
1526  
1527          $record = new stdClass();
1528          $record->course = $course->id;
1529          $record->showtype = 1;
1530          $resource = self::getDataGenerator()->create_module('resource', $record);
1531  
1532          $result = core_course_external::get_course_contents($course->id);
1533          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1534          $this->assertCount(1, $result[0]['modules']);   // One module, first section.
1535          $customdata = json_decode($result[0]['modules'][0]['customdata']);
1536          $displayoptions = unserialize($customdata->displayoptions);
1537          $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
1538      }
1539  
1540      /**
1541       * Test contents info is returned.
1542       */
1543      public function test_get_course_contents_contentsinfo() {
1544          global $USER;
1545  
1546          $this->resetAfterTest(true);
1547          $this->setAdminUser();
1548          $timenow = time();
1549  
1550          $course = self::getDataGenerator()->create_course();
1551  
1552          $record = new stdClass();
1553          $record->course = $course->id;
1554          // One resource with one file.
1555          $resource1 = self::getDataGenerator()->create_module('resource', $record);
1556  
1557          // More type of files.
1558          $record->files = file_get_unused_draft_itemid();
1559          $usercontext = context_user::instance($USER->id);
1560          $extensions = array('txt', 'png', 'pdf');
1561          $fs = get_file_storage();
1562          foreach ($extensions as $key => $extension) {
1563              // Add actual file there.
1564              $filerecord = array('component' => 'user', 'filearea' => 'draft',
1565                      'contextid' => $usercontext->id, 'itemid' => $record->files,
1566                      'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
1567              $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
1568          }
1569  
1570          // Create file reference.
1571          $repos = repository::get_instances(array('type' => 'user'));
1572          $userrepository = reset($repos);
1573  
1574          // Create a user private file.
1575          $userfilerecord = new stdClass;
1576          $userfilerecord->contextid = $usercontext->id;
1577          $userfilerecord->component = 'user';
1578          $userfilerecord->filearea  = 'private';
1579          $userfilerecord->itemid    = 0;
1580          $userfilerecord->filepath  = '/';
1581          $userfilerecord->filename  = 'userfile.txt';
1582          $userfilerecord->source    = 'test';
1583          $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
1584          $userfileref = $fs->pack_reference($userfilerecord);
1585  
1586          // Clone latest "normal" file.
1587          $filerefrecord = clone (object) $filerecord;
1588          $filerefrecord->filename = 'testref.txt';
1589          $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
1590          // Set main file pointing to the file reference.
1591          file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
1592              $filerefrecord->filename, 1);
1593  
1594          // Once the reference has been created, create the file resource.
1595          $resource2 = self::getDataGenerator()->create_module('resource', $record);
1596  
1597          $result = core_course_external::get_course_contents($course->id);
1598          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1599          $this->assertCount(2, $result[0]['modules']);
1600          foreach ($result[0]['modules'] as $module) {
1601              if ($module['instance'] == $resource1->id) {
1602                  $this->assertEquals(1, $module['contentsinfo']['filescount']);
1603                  $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1604                  $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
1605                  $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
1606              } else {
1607                  $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
1608                  $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
1609                      $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
1610                  $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
1611                  $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
1612                  $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1613                  $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
1614              }
1615          }
1616      }
1617  
1618      /**
1619       * Test get_course_contents when hidden sections are displayed.
1620       */
1621      public function test_get_course_contents_hiddensections() {
1622          global $DB;
1623          $this->resetAfterTest(true);
1624  
1625          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1626          // Force returning hidden sections.
1627          $course->hiddensections = 0;
1628          update_course($course);
1629  
1630          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1631          $user = self::getDataGenerator()->create_user();
1632          self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1633          $this->setUser($user);
1634  
1635          $sections = core_course_external::get_course_contents($course->id, array());
1636          // We need to execute the return values cleaning process to simulate the web service server.
1637          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1638  
1639          $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
1640          $this->assertCount(6, $sections[0]['modules']);
1641          $this->assertCount(1, $sections[1]['modules']);
1642          $this->assertCount(1, $sections[2]['modules']);
1643          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1644          $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1645  
1646          $this->assertNotEmpty($sections[3]['availabilityinfo']);
1647          $this->assertEquals(1, $sections[1]['section']);
1648          $this->assertEquals(2, $sections[2]['section']);
1649          $this->assertEquals(3, $sections[3]['section']);
1650          // The module with the availability restriction met is returning contents.
1651          $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1652          // The module with the availability restriction not met is not returning contents.
1653          $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1654  
1655          // Now include flag for returning stealth information (fake section).
1656          $sections = core_course_external::get_course_contents($course->id,
1657              array(array("name" => "includestealthmodules", "value" => 1)));
1658          // We need to execute the return values cleaning process to simulate the web service server.
1659          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1660  
1661          $this->assertCount(6, $sections); // Include fake section with stealth activities.
1662          $this->assertCount(6, $sections[0]['modules']);
1663          $this->assertCount(1, $sections[1]['modules']);
1664          $this->assertCount(1, $sections[2]['modules']);
1665          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1666          $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1667          $this->assertCount(1, $sections[5]['modules']); // One stealth module.
1668          $this->assertEquals(-1, $sections[5]['id']);
1669      }
1670  
1671      /**
1672       * Test duplicate_course
1673       */
1674      public function test_duplicate_course() {
1675          $this->resetAfterTest(true);
1676  
1677          // Create one course with three modules.
1678          $course  = self::getDataGenerator()->create_course();
1679          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1680          $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1681          $forumcontext = context_module::instance($forum->cmid);
1682          $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1683          $datacontext = context_module::instance($data->cmid);
1684          $datacm = get_coursemodule_from_instance('page', $data->id);
1685          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1686          $pagecontext = context_module::instance($page->cmid);
1687          $pagecm = get_coursemodule_from_instance('page', $page->id);
1688  
1689          // Set the required capabilities by the external function.
1690          $coursecontext = context_course::instance($course->id);
1691          $categorycontext = context_coursecat::instance($course->category);
1692          $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1693          $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1694          $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1695          $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1696          $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1697          // Optional capabilities to copy user data.
1698          $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1699          $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1700  
1701          $newcourse['fullname'] = 'Course duplicate';
1702          $newcourse['shortname'] = 'courseduplicate';
1703          $newcourse['categoryid'] = $course->category;
1704          $newcourse['visible'] = true;
1705          $newcourse['options'][] = array('name' => 'users', 'value' => true);
1706  
1707          $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1708                  $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1709  
1710          // We need to execute the return values cleaning process to simulate the web service server.
1711          $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1712  
1713          // Check that the course has been duplicated.
1714          $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1715      }
1716  
1717      /**
1718       * Test update_courses
1719       */
1720      public function test_update_courses() {
1721          global $DB, $CFG, $USER, $COURSE;
1722  
1723          // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1724          // trick because we are both updating and getting (for testing) course information
1725          // in the same request and core_course_external::update_courses()
1726          // is overwriting $COURSE all over the time with OLD values, so later
1727          // use of get_course() fetches those OLD values instead of the updated ones.
1728          // See MDL-39723 for more info.
1729          $origcourse = clone($COURSE);
1730  
1731          $this->resetAfterTest(true);
1732  
1733          // Set the required capabilities by the external function.
1734          $contextid = context_system::instance()->id;
1735          $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
1736          $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1737          $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1738          $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1739          $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1740          $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1741          $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1742          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
1743          $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
1744          $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
1745  
1746          // Create category and courses.
1747          $category1  = self::getDataGenerator()->create_category();
1748          $category2  = self::getDataGenerator()->create_category();
1749  
1750          $originalcourse1 = self::getDataGenerator()->create_course();
1751          self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
1752  
1753          $originalcourse2 = self::getDataGenerator()->create_course();
1754          self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
1755  
1756          // Course with custom fields.
1757          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
1758          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
1759              'categoryid' => $fieldcategory->get('id'),
1760              'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL, 'locked' => 1]];
1761          $field = self::getDataGenerator()->create_custom_field($customfield);
1762  
1763          $originalcourse3 = self::getDataGenerator()->create_course(['customfield_test' => 'Test value']);
1764          self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
1765  
1766          // Course values to be updated.
1767          $course1['id'] = $originalcourse1->id;
1768          $course1['fullname'] = 'Updated test course 1';
1769          $course1['shortname'] = 'Udestedtestcourse1';
1770          $course1['categoryid'] = $category1->id;
1771  
1772          $course2['id'] = $originalcourse2->id;
1773          $course2['fullname'] = 'Updated test course 2';
1774          $course2['shortname'] = 'Updestedtestcourse2';
1775          $course2['categoryid'] = $category2->id;
1776          $course2['idnumber'] = 'Updatedidnumber2';
1777          $course2['summary'] = 'Updaated description for course 2';
1778          $course2['summaryformat'] = FORMAT_HTML;
1779          $course2['format'] = 'topics';
1780          $course2['showgrades'] = 1;
1781          $course2['newsitems'] = 3;
1782          $course2['startdate'] = 1420092000; // 01/01/2015.
1783          $course2['enddate'] = 1422669600; // 01/31/2015.
1784          $course2['maxbytes'] = 100000;
1785          $course2['showreports'] = 1;
1786          $course2['visible'] = 0;
1787          $course2['hiddensections'] = 0;
1788          $course2['groupmode'] = 0;
1789          $course2['groupmodeforce'] = 0;
1790          $course2['defaultgroupingid'] = 0;
1791          $course2['enablecompletion'] = 1;
1792          $course2['lang'] = 'en';
1793          $course2['forcetheme'] = 'classic';
1794  
1795          $course3['id'] = $originalcourse3->id;
1796          $updatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'Updated test value'];
1797          $course3['customfields'] = [$updatedcustomfieldvalue];
1798          $courses = array($course1, $course2, $course3);
1799  
1800          $updatedcoursewarnings = core_course_external::update_courses($courses);
1801          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1802                  $updatedcoursewarnings);
1803          $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
1804  
1805          // Check that right number of courses were created.
1806          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1807  
1808          // Check that the courses were correctly created.
1809          foreach ($courses as $course) {
1810              $courseinfo = course_get_format($course['id'])->get_course();
1811              $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
1812              if ($course['id'] == $course2['id']) {
1813                  $this->assertEquals($course2['fullname'], $courseinfo->fullname);
1814                  $this->assertEquals($course2['shortname'], $courseinfo->shortname);
1815                  $this->assertEquals($course2['categoryid'], $courseinfo->category);
1816                  $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
1817                  $this->assertEquals($course2['summary'], $courseinfo->summary);
1818                  $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
1819                  $this->assertEquals($course2['format'], $courseinfo->format);
1820                  $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
1821                  $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
1822                  $this->assertEquals($course2['startdate'], $courseinfo->startdate);
1823                  $this->assertEquals($course2['enddate'], $courseinfo->enddate);
1824                  $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
1825                  $this->assertEquals($course2['showreports'], $courseinfo->showreports);
1826                  $this->assertEquals($course2['visible'], $courseinfo->visible);
1827                  $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
1828                  $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
1829                  $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
1830                  $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
1831                  $this->assertEquals($course2['lang'], $courseinfo->lang);
1832  
1833                  if (!empty($CFG->allowcoursethemes)) {
1834                      $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
1835                  }
1836  
1837                  $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
1838                  $this->assertEquals(['test' => null], (array)$customfields);
1839              } else if ($course['id'] == $course1['id']) {
1840                  $this->assertEquals($course1['fullname'], $courseinfo->fullname);
1841                  $this->assertEquals($course1['shortname'], $courseinfo->shortname);
1842                  $this->assertEquals($course1['categoryid'], $courseinfo->category);
1843                  $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1844                  $this->assertEquals('topics', $courseinfo->format);
1845                  $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
1846                  $this->assertEquals(0, $courseinfo->newsitems);
1847                  $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
1848                  $this->assertEquals(['test' => null], (array)$customfields);
1849              } else if ($course['id'] == $course3['id']) {
1850                  $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
1851              } else {
1852                  throw new moodle_exception('Unexpected shortname');
1853              }
1854          }
1855  
1856          $courses = array($course1);
1857          // Try update course without update capability.
1858          $user = self::getDataGenerator()->create_user();
1859          $this->setUser($user);
1860          $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
1861          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1862          $updatedcoursewarnings = core_course_external::update_courses($courses);
1863          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1864                                                                      $updatedcoursewarnings);
1865          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1866  
1867          // Try update course category without capability.
1868          $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
1869          $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1870          $user = self::getDataGenerator()->create_user();
1871          $this->setUser($user);
1872          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1873          $course1['categoryid'] = $category2->id;
1874          $courses = array($course1);
1875          $updatedcoursewarnings = core_course_external::update_courses($courses);
1876          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1877                                                                      $updatedcoursewarnings);
1878          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1879  
1880          // Try update course fullname without capability.
1881          $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1882          $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1883          $user = self::getDataGenerator()->create_user();
1884          $this->setUser($user);
1885          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1886          $updatedcoursewarnings = core_course_external::update_courses($courses);
1887          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1888                                                                      $updatedcoursewarnings);
1889          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1890          $course1['fullname'] = 'Testing fullname without permission';
1891          $courses = array($course1);
1892          $updatedcoursewarnings = core_course_external::update_courses($courses);
1893          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1894                                                                      $updatedcoursewarnings);
1895          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1896  
1897          // Try update course shortname without capability.
1898          $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1899          $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1900          $user = self::getDataGenerator()->create_user();
1901          $this->setUser($user);
1902          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1903          $updatedcoursewarnings = core_course_external::update_courses($courses);
1904          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1905                                                                      $updatedcoursewarnings);
1906          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1907          $course1['shortname'] = 'Testing shortname without permission';
1908          $courses = array($course1);
1909          $updatedcoursewarnings = core_course_external::update_courses($courses);
1910          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1911                                                                      $updatedcoursewarnings);
1912          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1913  
1914          // Try update course idnumber without capability.
1915          $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1916          $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1917          $user = self::getDataGenerator()->create_user();
1918          $this->setUser($user);
1919          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1920          $updatedcoursewarnings = core_course_external::update_courses($courses);
1921          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1922                                                                      $updatedcoursewarnings);
1923          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1924          $course1['idnumber'] = 'NEWIDNUMBER';
1925          $courses = array($course1);
1926          $updatedcoursewarnings = core_course_external::update_courses($courses);
1927          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1928                                                                      $updatedcoursewarnings);
1929          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1930  
1931          // Try update course summary without capability.
1932          $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1933          $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1934          $user = self::getDataGenerator()->create_user();
1935          $this->setUser($user);
1936          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1937          $updatedcoursewarnings = core_course_external::update_courses($courses);
1938          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1939                                                                      $updatedcoursewarnings);
1940          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1941          $course1['summary'] = 'New summary';
1942          $courses = array($course1);
1943          $updatedcoursewarnings = core_course_external::update_courses($courses);
1944          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1945                                                                      $updatedcoursewarnings);
1946          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1947  
1948          // Try update course with invalid summary format.
1949          $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1950          $user = self::getDataGenerator()->create_user();
1951          $this->setUser($user);
1952          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1953          $updatedcoursewarnings = core_course_external::update_courses($courses);
1954          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1955                                                                      $updatedcoursewarnings);
1956          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1957          $course1['summaryformat'] = 10;
1958          $courses = array($course1);
1959          $updatedcoursewarnings = core_course_external::update_courses($courses);
1960          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1961                                                                      $updatedcoursewarnings);
1962          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1963  
1964          // Try update course visibility without capability.
1965          $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
1966          $user = self::getDataGenerator()->create_user();
1967          $this->setUser($user);
1968          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
1969          $course1['summaryformat'] = FORMAT_MOODLE;
1970          $courses = array($course1);
1971          $updatedcoursewarnings = core_course_external::update_courses($courses);
1972          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1973                                                                      $updatedcoursewarnings);
1974          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1975          $course1['visible'] = 0;
1976          $courses = array($course1);
1977          $updatedcoursewarnings = core_course_external::update_courses($courses);
1978          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1979                                                                      $updatedcoursewarnings);
1980          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
1981  
1982          // Try update course custom fields without capability.
1983          $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1984          $user = self::getDataGenerator()->create_user();
1985          $this->setUser($user);
1986          self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
1987  
1988          $newupdatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'New updated value'];
1989          $course3['customfields'] = [$newupdatedcustomfieldvalue];
1990  
1991          core_course_external::update_courses([$course3]);
1992  
1993          // Custom field was not updated.
1994          $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
1995          $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
1996      }
1997  
1998      /**
1999       * Test delete course_module.
2000       */
2001      public function test_delete_modules() {
2002          global $DB;
2003  
2004          // Ensure we reset the data after this test.
2005          $this->resetAfterTest(true);
2006  
2007          // Create a user.
2008          $user = self::getDataGenerator()->create_user();
2009  
2010          // Set the tests to run as the user.
2011          self::setUser($user);
2012  
2013          // Create a course to add the modules.
2014          $course = self::getDataGenerator()->create_course();
2015  
2016          // Create two test modules.
2017          $record = new stdClass();
2018          $record->course = $course->id;
2019          $module1 = self::getDataGenerator()->create_module('forum', $record);
2020          $module2 = self::getDataGenerator()->create_module('assign', $record);
2021  
2022          // Check the forum was correctly created.
2023          $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
2024  
2025          // Check the assignment was correctly created.
2026          $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
2027  
2028          // Check data exists in the course modules table.
2029          $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2030                  array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2031  
2032          // Enrol the user in the course.
2033          $enrol = enrol_get_plugin('manual');
2034          $enrolinstances = enrol_get_instances($course->id, true);
2035          foreach ($enrolinstances as $courseenrolinstance) {
2036              if ($courseenrolinstance->enrol == "manual") {
2037                  $instance = $courseenrolinstance;
2038                  break;
2039              }
2040          }
2041          $enrol->enrol_user($instance, $user->id);
2042  
2043          // Assign capabilities to delete module 1.
2044          $modcontext = context_module::instance($module1->cmid);
2045          $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
2046  
2047          // Assign capabilities to delete module 2.
2048          $modcontext = context_module::instance($module2->cmid);
2049          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
2050          $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
2051  
2052          // Deleting these module instances.
2053          core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2054  
2055          // Check the forum was deleted.
2056          $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
2057  
2058          // Check the assignment was deleted.
2059          $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
2060  
2061          // Check we retrieve no data in the course modules table.
2062          $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2063                  array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2064  
2065          // Call with non-existent course module id and ensure exception thrown.
2066          try {
2067              core_course_external::delete_modules(array('1337'));
2068              $this->fail('Exception expected due to missing course module.');
2069          } catch (dml_missing_record_exception $e) {
2070              $this->assertEquals('invalidcoursemodule', $e->errorcode);
2071          }
2072  
2073          // Create two modules.
2074          $module1 = self::getDataGenerator()->create_module('forum', $record);
2075          $module2 = self::getDataGenerator()->create_module('assign', $record);
2076  
2077          // Since these modules were recreated the user will not have capabilities
2078          // to delete them, ensure exception is thrown if they try.
2079          try {
2080              core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2081              $this->fail('Exception expected due to missing capability.');
2082          } catch (moodle_exception $e) {
2083              $this->assertEquals('nopermissions', $e->errorcode);
2084          }
2085  
2086          // Unenrol user from the course.
2087          $enrol->unenrol_user($instance, $user->id);
2088  
2089          // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
2090          try {
2091              core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2092              $this->fail('Exception expected due to being unenrolled from the course.');
2093          } catch (moodle_exception $e) {
2094              $this->assertEquals('requireloginerror', $e->errorcode);
2095          }
2096      }
2097  
2098      /**
2099       * Test import_course into an empty course
2100       */
2101      public function test_import_course_empty() {
2102          global $USER;
2103  
2104          $this->resetAfterTest(true);
2105  
2106          $course1  = self::getDataGenerator()->create_course();
2107          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
2108          $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
2109  
2110          $course2  = self::getDataGenerator()->create_course();
2111  
2112          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2113          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2114  
2115          // Verify the state of the courses before we do the import.
2116          $this->assertCount(2, $course1cms);
2117          $this->assertEmpty($course2cms);
2118  
2119          // Setup the user to run the operation (ugly hack because validate_context() will
2120          // fail as the email is not set by $this->setAdminUser()).
2121          $this->setAdminUser();
2122          $USER->email = 'emailtopass@example.com';
2123  
2124          // Import from course1 to course2.
2125          core_course_external::import_course($course1->id, $course2->id, 0);
2126  
2127          // Verify that now we have two modules in both courses.
2128          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2129          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2130          $this->assertCount(2, $course1cms);
2131          $this->assertCount(2, $course2cms);
2132  
2133          // Verify that the names transfered across correctly.
2134          foreach ($course2cms as $cm) {
2135              if ($cm->modname === 'page') {
2136                  $this->assertEquals($cm->name, $page->name);
2137              } else if ($cm->modname === 'forum') {
2138                  $this->assertEquals($cm->name, $forum->name);
2139              } else {
2140                  $this->fail('Unknown CM found.');
2141              }
2142          }
2143      }
2144  
2145      /**
2146       * Test import_course into an filled course
2147       */
2148      public function test_import_course_filled() {
2149          global $USER;
2150  
2151          $this->resetAfterTest(true);
2152  
2153          // Add forum and page to course1.
2154          $course1  = self::getDataGenerator()->create_course();
2155          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2156          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2157  
2158          // Add quiz to course 2.
2159          $course2  = self::getDataGenerator()->create_course();
2160          $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2161  
2162          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2163          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2164  
2165          // Verify the state of the courses before we do the import.
2166          $this->assertCount(2, $course1cms);
2167          $this->assertCount(1, $course2cms);
2168  
2169          // Setup the user to run the operation (ugly hack because validate_context() will
2170          // fail as the email is not set by $this->setAdminUser()).
2171          $this->setAdminUser();
2172          $USER->email = 'emailtopass@example.com';
2173  
2174          // Import from course1 to course2 without deleting content.
2175          core_course_external::import_course($course1->id, $course2->id, 0);
2176  
2177          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2178  
2179          // Verify that now we have three modules in course2.
2180          $this->assertCount(3, $course2cms);
2181  
2182          // Verify that the names transfered across correctly.
2183          foreach ($course2cms as $cm) {
2184              if ($cm->modname === 'page') {
2185                  $this->assertEquals($cm->name, $page->name);
2186              } else if ($cm->modname === 'forum') {
2187                  $this->assertEquals($cm->name, $forum->name);
2188              } else if ($cm->modname === 'quiz') {
2189                  $this->assertEquals($cm->name, $quiz->name);
2190              } else {
2191                  $this->fail('Unknown CM found.');
2192              }
2193          }
2194      }
2195  
2196      /**
2197       * Test import_course with only blocks set to backup
2198       */
2199      public function test_import_course_blocksonly() {
2200          global $USER, $DB;
2201  
2202          $this->resetAfterTest(true);
2203  
2204          // Add forum and page to course1.
2205          $course1  = self::getDataGenerator()->create_course();
2206          $course1ctx = context_course::instance($course1->id);
2207          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2208          $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
2209  
2210          $course2  = self::getDataGenerator()->create_course();
2211          $course2ctx = context_course::instance($course2->id);
2212          $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2213          $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2214  
2215          // Setup the user to run the operation (ugly hack because validate_context() will
2216          // fail as the email is not set by $this->setAdminUser()).
2217          $this->setAdminUser();
2218          $USER->email = 'emailtopass@example.com';
2219  
2220          // Import from course1 to course2 without deleting content, but excluding
2221          // activities.
2222          $options = array(
2223              array('name' => 'activities', 'value' => 0),
2224              array('name' => 'blocks', 'value' => 1),
2225              array('name' => 'filters', 'value' => 0),
2226          );
2227  
2228          core_course_external::import_course($course1->id, $course2->id, 0, $options);
2229  
2230          $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2231          $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2232          // Check that course modules haven't changed, but that blocks have.
2233          $this->assertEquals($initialcmcount, $newcmcount);
2234          $this->assertEquals(($initialblockcount + 1), $newblockcount);
2235      }
2236  
2237      /**
2238       * Test import_course into an filled course, deleting content.
2239       */
2240      public function test_import_course_deletecontent() {
2241          global $USER;
2242          $this->resetAfterTest(true);
2243  
2244          // Add forum and page to course1.
2245          $course1  = self::getDataGenerator()->create_course();
2246          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2247          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2248  
2249          // Add quiz to course 2.
2250          $course2  = self::getDataGenerator()->create_course();
2251          $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2252  
2253          $course1cms = get_fast_modinfo($course1->id)->get_cms();
2254          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2255  
2256          // Verify the state of the courses before we do the import.
2257          $this->assertCount(2, $course1cms);
2258          $this->assertCount(1, $course2cms);
2259  
2260          // Setup the user to run the operation (ugly hack because validate_context() will
2261          // fail as the email is not set by $this->setAdminUser()).
2262          $this->setAdminUser();
2263          $USER->email = 'emailtopass@example.com';
2264  
2265          // Import from course1 to course2,  deleting content.
2266          core_course_external::import_course($course1->id, $course2->id, 1);
2267  
2268          $course2cms = get_fast_modinfo($course2->id)->get_cms();
2269  
2270          // Verify that now we have two modules in course2.
2271          $this->assertCount(2, $course2cms);
2272  
2273          // Verify that the course only contains the imported modules.
2274          foreach ($course2cms as $cm) {
2275              if ($cm->modname === 'page') {
2276                  $this->assertEquals($cm->name, $page->name);
2277              } else if ($cm->modname === 'forum') {
2278                  $this->assertEquals($cm->name, $forum->name);
2279              } else {
2280                  $this->fail('Unknown CM found: '.$cm->name);
2281              }
2282          }
2283      }
2284  
2285      /**
2286       * Ensure import_course handles incorrect deletecontent option correctly.
2287       */
2288      public function test_import_course_invalid_deletecontent_option() {
2289          $this->resetAfterTest(true);
2290  
2291          $course1  = self::getDataGenerator()->create_course();
2292          $course2  = self::getDataGenerator()->create_course();
2293  
2294          $this->expectException('moodle_exception');
2295          $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
2296          // Import from course1 to course2, with invalid option
2297          core_course_external::import_course($course1->id, $course2->id, -1);;
2298      }
2299  
2300      /**
2301       * Test view_course function
2302       */
2303      public function test_view_course() {
2304  
2305          $this->resetAfterTest();
2306  
2307          // Course without sections.
2308          $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
2309          $this->setAdminUser();
2310  
2311          // Redirect events to the sink, so we can recover them later.
2312          $sink = $this->redirectEvents();
2313  
2314          $result = core_course_external::view_course($course->id, 1);
2315          $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2316          $events = $sink->get_events();
2317          $event = reset($events);
2318  
2319          // Check the event details are correct.
2320          $this->assertInstanceOf('\core\event\course_viewed', $event);
2321          $this->assertEquals(context_course::instance($course->id), $event->get_context());
2322          $this->assertEquals(1, $event->other['coursesectionnumber']);
2323  
2324          $result = core_course_external::view_course($course->id);
2325          $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2326          $events = $sink->get_events();
2327          $event = array_pop($events);
2328          $sink->close();
2329  
2330          // Check the event details are correct.
2331          $this->assertInstanceOf('\core\event\course_viewed', $event);
2332          $this->assertEquals(context_course::instance($course->id), $event->get_context());
2333          $this->assertEmpty($event->other);
2334  
2335      }
2336  
2337      /**
2338       * Test get_course_module
2339       */
2340      public function test_get_course_module() {
2341          global $DB;
2342  
2343          $this->resetAfterTest(true);
2344  
2345          $this->setAdminUser();
2346          $course = self::getDataGenerator()->create_course();
2347          $record = array(
2348              'course' => $course->id,
2349              'name' => 'First Assignment'
2350          );
2351          $options = array(
2352              'idnumber' => 'ABC',
2353              'visible' => 0
2354          );
2355          // Hidden activity.
2356          $assign = self::getDataGenerator()->create_module('assign', $record, $options);
2357  
2358          $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
2359  
2360          // Insert a custom grade scale to be used by an outcome.
2361          $gradescale = new grade_scale();
2362          $gradescale->name        = 'gettcoursemodulescale';
2363          $gradescale->courseid    = $course->id;
2364          $gradescale->userid      = 0;
2365          $gradescale->scale       = $outcomescale;
2366          $gradescale->description = 'This scale is used to mark standard assignments.';
2367          $gradescale->insert();
2368  
2369          // Insert an outcome.
2370          $data = new stdClass();
2371          $data->courseid = $course->id;
2372          $data->fullname = 'Team work';
2373          $data->shortname = 'Team work';
2374          $data->scaleid = $gradescale->id;
2375          $outcome = new grade_outcome($data, false);
2376          $outcome->insert();
2377  
2378          $outcomegradeitem = new grade_item();
2379          $outcomegradeitem->itemname = $outcome->shortname;
2380          $outcomegradeitem->itemtype = 'mod';
2381          $outcomegradeitem->itemmodule = 'assign';
2382          $outcomegradeitem->iteminstance = $assign->id;
2383          $outcomegradeitem->outcomeid = $outcome->id;
2384          $outcomegradeitem->cmid = 0;
2385          $outcomegradeitem->courseid = $course->id;
2386          $outcomegradeitem->aggregationcoef = 0;
2387          $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
2388          $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
2389          $outcomegradeitem->scaleid = $outcome->scaleid;
2390          $outcomegradeitem->insert();
2391  
2392          $assignmentgradeitem = grade_item::fetch(
2393              array(
2394                  'itemtype' => 'mod',
2395                  'itemmodule' => 'assign',
2396                  'iteminstance' => $assign->id,
2397                  'itemnumber' => 0,
2398                  'courseid' => $course->id
2399              )
2400          );
2401          $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
2402          $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
2403  
2404          // Test admin user can see the complete hidden activity.
2405          $result = core_course_external::get_course_module($assign->cmid);
2406          $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2407  
2408          $this->assertCount(0, $result['warnings']);
2409          // Test we retrieve all the fields.
2410          $this->assertCount(28, $result['cm']);
2411          $this->assertEquals($record['name'], $result['cm']['name']);
2412          $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2413          $this->assertEquals(100, $result['cm']['grade']);
2414          $this->assertEquals(0.0, $result['cm']['gradepass']);
2415          $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
2416          $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
2417          $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
2418  
2419          $student = $this->getDataGenerator()->create_user();
2420          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2421  
2422          self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2423          $this->setUser($student);
2424  
2425          // The user shouldn't be able to see the activity.
2426          try {
2427              core_course_external::get_course_module($assign->cmid);
2428              $this->fail('Exception expected due to invalid permissions.');
2429          } catch (moodle_exception $e) {
2430              $this->assertEquals('requireloginerror', $e->errorcode);
2431          }
2432  
2433          // Make module visible.
2434          set_coursemodule_visible($assign->cmid, 1);
2435  
2436          // Test student user.
2437          $result = core_course_external::get_course_module($assign->cmid);
2438          $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2439  
2440          $this->assertCount(0, $result['warnings']);
2441          // Test we retrieve only the few files we can see.
2442          $this->assertCount(11, $result['cm']);
2443          $this->assertEquals($assign->cmid, $result['cm']['id']);
2444          $this->assertEquals($course->id, $result['cm']['course']);
2445          $this->assertEquals('assign', $result['cm']['modname']);
2446          $this->assertEquals($assign->id, $result['cm']['instance']);
2447  
2448      }
2449  
2450      /**
2451       * Test get_course_module_by_instance
2452       */
2453      public function test_get_course_module_by_instance() {
2454          global $DB;
2455  
2456          $this->resetAfterTest(true);
2457  
2458          $this->setAdminUser();
2459          $course = self::getDataGenerator()->create_course();
2460          $record = array(
2461              'course' => $course->id,
2462              'name' => 'First quiz',
2463              'grade' => 90.00
2464          );
2465          $options = array(
2466              'idnumber' => 'ABC',
2467              'visible' => 0
2468          );
2469          // Hidden activity.
2470          $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
2471  
2472          // Test admin user can see the complete hidden activity.
2473          $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2474          $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2475  
2476          $this->assertCount(0, $result['warnings']);
2477          // Test we retrieve all the fields.
2478          $this->assertCount(26, $result['cm']);
2479          $this->assertEquals($record['name'], $result['cm']['name']);
2480          $this->assertEquals($record['grade'], $result['cm']['grade']);
2481          $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2482  
2483          $student = $this->getDataGenerator()->create_user();
2484          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2485  
2486          self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2487          $this->setUser($student);
2488  
2489          // The user shouldn't be able to see the activity.
2490          try {
2491              core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2492              $this->fail('Exception expected due to invalid permissions.');
2493          } catch (moodle_exception $e) {
2494              $this->assertEquals('requireloginerror', $e->errorcode);
2495          }
2496  
2497          // Make module visible.
2498          set_coursemodule_visible($quiz->cmid, 1);
2499  
2500          // Test student user.
2501          $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2502          $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2503  
2504          $this->assertCount(0, $result['warnings']);
2505          // Test we retrieve only the few files we can see.
2506          $this->assertCount(11, $result['cm']);
2507          $this->assertEquals($quiz->cmid, $result['cm']['id']);
2508          $this->assertEquals($course->id, $result['cm']['course']);
2509          $this->assertEquals('quiz', $result['cm']['modname']);
2510          $this->assertEquals($quiz->id, $result['cm']['instance']);
2511  
2512          // Try with an invalid module name.
2513          try {
2514              core_course_external::get_course_module_by_instance('abc', $quiz->id);
2515              $this->fail('Exception expected due to invalid module name.');
2516          } catch (dml_read_exception $e) {
2517              $this->assertEquals('dmlreadexception', $e->errorcode);
2518          }
2519  
2520      }
2521  
2522      /**
2523       * Test get_user_navigation_options
2524       */
2525      public function test_get_user_navigation_options() {
2526          global $USER;
2527  
2528          $this->resetAfterTest();
2529          $course1 = self::getDataGenerator()->create_course();
2530          $course2 = self::getDataGenerator()->create_course();
2531  
2532          // Create a viewer user.
2533          $viewer = self::getDataGenerator()->create_user();
2534          $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2535          $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2536  
2537          $this->setUser($viewer->id);
2538          $courses = array($course1->id , $course2->id, SITEID);
2539  
2540          $result = core_course_external::get_user_navigation_options($courses);
2541          $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2542  
2543          $this->assertCount(0, $result['warnings']);
2544          $this->assertCount(3, $result['courses']);
2545  
2546          foreach ($result['courses'] as $course) {
2547              $navoptions = new stdClass;
2548              foreach ($course['options'] as $option) {
2549                  $navoptions->{$option['name']} = $option['available'];
2550              }
2551              $this->assertCount(9, $course['options']);
2552              if ($course['id'] == SITEID) {
2553                  $this->assertTrue($navoptions->blogs);
2554                  $this->assertFalse($navoptions->notes);
2555                  $this->assertFalse($navoptions->participants);
2556                  $this->assertTrue($navoptions->badges);
2557                  $this->assertTrue($navoptions->tags);
2558                  $this->assertFalse($navoptions->grades);
2559                  $this->assertFalse($navoptions->search);
2560                  $this->assertTrue($navoptions->calendar);
2561                  $this->assertTrue($navoptions->competencies);
2562              } else {
2563                  $this->assertTrue($navoptions->blogs);
2564                  $this->assertFalse($navoptions->notes);
2565                  $this->assertTrue($navoptions->participants);
2566                  $this->assertFalse($navoptions->badges);
2567                  $this->assertFalse($navoptions->tags);
2568                  $this->assertTrue($navoptions->grades);
2569                  $this->assertFalse($navoptions->search);
2570                  $this->assertFalse($navoptions->calendar);
2571                  $this->assertTrue($navoptions->competencies);
2572              }
2573          }
2574      }
2575  
2576      /**
2577       * Test get_user_administration_options
2578       */
2579      public function test_get_user_administration_options() {
2580          global $USER;
2581  
2582          $this->resetAfterTest();
2583          $course1 = self::getDataGenerator()->create_course();
2584          $course2 = self::getDataGenerator()->create_course();
2585  
2586          // Create a viewer user.
2587          $viewer = self::getDataGenerator()->create_user();
2588          $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2589          $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2590  
2591          $this->setUser($viewer->id);
2592          $courses = array($course1->id , $course2->id, SITEID);
2593  
2594          $result = core_course_external::get_user_administration_options($courses);
2595          $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2596  
2597          $this->assertCount(0, $result['warnings']);
2598          $this->assertCount(3, $result['courses']);
2599  
2600          foreach ($result['courses'] as $course) {
2601              $adminoptions = new stdClass;
2602              foreach ($course['options'] as $option) {
2603                  $adminoptions->{$option['name']} = $option['available'];
2604              }
2605              if ($course['id'] == SITEID) {
2606                  $this->assertCount(17, $course['options']);
2607                  $this->assertFalse($adminoptions->update);
2608                  $this->assertFalse($adminoptions->filters);
2609                  $this->assertFalse($adminoptions->reports);
2610                  $this->assertFalse($adminoptions->backup);
2611                  $this->assertFalse($adminoptions->restore);
2612                  $this->assertFalse($adminoptions->files);
2613                  $this->assertFalse(!isset($adminoptions->tags));
2614                  $this->assertFalse($adminoptions->gradebook);
2615                  $this->assertFalse($adminoptions->outcomes);
2616                  $this->assertFalse($adminoptions->badges);
2617                  $this->assertFalse($adminoptions->import);
2618                  $this->assertFalse($adminoptions->reset);
2619                  $this->assertFalse($adminoptions->roles);
2620                  $this->assertFalse($adminoptions->editcompletion);
2621                  $this->assertFalse($adminoptions->copy);
2622              } else {
2623                  $this->assertCount(15, $course['options']);
2624                  $this->assertFalse($adminoptions->update);
2625                  $this->assertFalse($adminoptions->filters);
2626                  $this->assertFalse($adminoptions->reports);
2627                  $this->assertFalse($adminoptions->backup);
2628                  $this->assertFalse($adminoptions->restore);
2629                  $this->assertFalse($adminoptions->files);
2630                  $this->assertFalse($adminoptions->tags);
2631                  $this->assertFalse($adminoptions->gradebook);
2632                  $this->assertFalse($adminoptions->outcomes);
2633                  $this->assertTrue($adminoptions->badges);
2634                  $this->assertFalse($adminoptions->import);
2635                  $this->assertFalse($adminoptions->reset);
2636                  $this->assertFalse($adminoptions->roles);
2637                  $this->assertFalse($adminoptions->editcompletion);
2638                  $this->assertFalse($adminoptions->copy);
2639              }
2640          }
2641      }
2642  
2643      /**
2644       * Test get_courses_by_fields
2645       */
2646      public function test_get_courses_by_field() {
2647          global $DB;
2648          $this->resetAfterTest(true);
2649  
2650          $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2651          $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
2652          $course1 = self::getDataGenerator()->create_course(
2653              array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
2654  
2655          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2656          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
2657              'categoryid' => $fieldcategory->get('id')];
2658          $field = self::getDataGenerator()->create_custom_field($customfield);
2659          $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
2660          $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2', 'customfields' => [$customfieldvalue]));
2661  
2662          $student1 = self::getDataGenerator()->create_user();
2663          $user1 = self::getDataGenerator()->create_user();
2664          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2665          self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2666          self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2667  
2668          self::setAdminUser();
2669          // As admins, we should be able to retrieve everything.
2670          $result = core_course_external::get_courses_by_field();
2671          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2672          $this->assertCount(3, $result['courses']);
2673          // Expect to receive all the fields.
2674          $this->assertCount(40, $result['courses'][0]);
2675          $this->assertCount(41, $result['courses'][1]);  // One more field because is not the site course.
2676          $this->assertCount(41, $result['courses'][2]);  // One more field because is not the site course.
2677  
2678          $result = core_course_external::get_courses_by_field('id', $course1->id);
2679          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2680          $this->assertCount(1, $result['courses']);
2681          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2682          // Expect to receive all the fields.
2683          $this->assertCount(41, $result['courses'][0]);
2684          // Check default values for course format topics.
2685          $this->assertCount(2, $result['courses'][0]['courseformatoptions']);
2686          foreach ($result['courses'][0]['courseformatoptions'] as $option) {
2687              if ($option['name'] == 'hiddensections') {
2688                  $this->assertEquals(0, $option['value']);
2689              } else {
2690                  $this->assertEquals('coursedisplay', $option['name']);
2691                  $this->assertEquals(0, $option['value']);
2692              }
2693          }
2694  
2695          $result = core_course_external::get_courses_by_field('id', $course2->id);
2696          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2697          $this->assertCount(1, $result['courses']);
2698          $this->assertEquals($course2->id, $result['courses'][0]['id']);
2699          // Check custom fields properly returned.
2700          $this->assertEquals([
2701              'shortname' => $customfield['shortname'],
2702              'name' => $customfield['name'],
2703              'type' => $customfield['type'],
2704              'value' => $customfieldvalue['value'],
2705              'valueraw' => $customfieldvalue['value'],
2706          ], $result['courses'][0]['customfields'][0]);
2707  
2708          $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2709          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2710          $this->assertCount(2, $result['courses']);
2711  
2712          // Check default filters.
2713          $this->assertCount(6, $result['courses'][0]['filters']);
2714          $this->assertCount(6, $result['courses'][1]['filters']);
2715  
2716          $result = core_course_external::get_courses_by_field('category', $category1->id);
2717          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2718          $this->assertCount(1, $result['courses']);
2719          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2720          $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
2721  
2722          $result = core_course_external::get_courses_by_field('shortname', 'c1');
2723          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2724          $this->assertCount(1, $result['courses']);
2725          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2726  
2727          $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2728          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2729          $this->assertCount(1, $result['courses']);
2730          $this->assertEquals($course2->id, $result['courses'][0]['id']);
2731  
2732          $result = core_course_external::get_courses_by_field('idnumber', 'x');
2733          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2734          $this->assertCount(0, $result['courses']);
2735  
2736          // Change filter value.
2737          filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
2738  
2739          self::setUser($student1);
2740          // All visible courses  (including front page) for normal student.
2741          $result = core_course_external::get_courses_by_field();
2742          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2743          $this->assertCount(2, $result['courses']);
2744          $this->assertCount(33, $result['courses'][0]);
2745          $this->assertCount(34, $result['courses'][1]);  // One field more (course format options), not present in site course.
2746  
2747          $result = core_course_external::get_courses_by_field('id', $course1->id);
2748          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2749          $this->assertCount(1, $result['courses']);
2750          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2751          // Expect to receive all the files that a student can see.
2752          $this->assertCount(34, $result['courses'][0]);
2753  
2754          // Check default filters.
2755          $filters = $result['courses'][0]['filters'];
2756          $this->assertCount(6, $filters);
2757          $found = false;
2758          foreach ($filters as $filter) {
2759              if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
2760                  $found = true;
2761              }
2762          }
2763          $this->assertTrue($found);
2764  
2765          // Course 2 is not visible.
2766          $result = core_course_external::get_courses_by_field('id', $course2->id);
2767          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2768          $this->assertCount(0, $result['courses']);
2769  
2770          $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2771          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2772          $this->assertCount(1, $result['courses']);
2773  
2774          $result = core_course_external::get_courses_by_field('category', $category1->id);
2775          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2776          $this->assertCount(1, $result['courses']);
2777          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2778  
2779          $result = core_course_external::get_courses_by_field('shortname', 'c1');
2780          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2781          $this->assertCount(1, $result['courses']);
2782          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2783  
2784          $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2785          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2786          $this->assertCount(0, $result['courses']);
2787  
2788          $result = core_course_external::get_courses_by_field('idnumber', 'x');
2789          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2790          $this->assertCount(0, $result['courses']);
2791  
2792          self::setUser($user1);
2793          // All visible courses (including front page) for authenticated user.
2794          $result = core_course_external::get_courses_by_field();
2795          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2796          $this->assertCount(2, $result['courses']);
2797          $this->assertCount(33, $result['courses'][0]);  // Site course.
2798          $this->assertCount(16, $result['courses'][1]);  // Only public information, not enrolled.
2799  
2800          $result = core_course_external::get_courses_by_field('id', $course1->id);
2801          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2802          $this->assertCount(1, $result['courses']);
2803          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2804          // Expect to receive all the files that a authenticated can see.
2805          $this->assertCount(16, $result['courses'][0]);
2806  
2807          // Course 2 is not visible.
2808          $result = core_course_external::get_courses_by_field('id', $course2->id);
2809          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2810          $this->assertCount(0, $result['courses']);
2811  
2812          $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2813          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2814          $this->assertCount(1, $result['courses']);
2815  
2816          $result = core_course_external::get_courses_by_field('category', $category1->id);
2817          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2818          $this->assertCount(1, $result['courses']);
2819          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2820  
2821          $result = core_course_external::get_courses_by_field('shortname', 'c1');
2822          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2823          $this->assertCount(1, $result['courses']);
2824          $this->assertEquals($course1->id, $result['courses'][0]['id']);
2825  
2826          $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2827          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2828          $this->assertCount(0, $result['courses']);
2829  
2830          $result = core_course_external::get_courses_by_field('idnumber', 'x');
2831          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2832          $this->assertCount(0, $result['courses']);
2833      }
2834  
2835      /**
2836       * Test retrieving courses by field returns custom field data
2837       */
2838      public function test_get_courses_by_field_customfields(): void {
2839          $this->resetAfterTest();
2840          $this->setAdminUser();
2841  
2842          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
2843          $datefield = $this->getDataGenerator()->create_custom_field([
2844              'categoryid' => $fieldcategory->get('id'),
2845              'shortname' => 'mydate',
2846              'name' => 'My date',
2847              'type' => 'date',
2848          ]);
2849  
2850          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
2851              [
2852                  'shortname' => $datefield->get('shortname'),
2853                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
2854              ],
2855          ]]);
2856  
2857          $result = external_api::clean_returnvalue(
2858              core_course_external::get_courses_by_field_returns(),
2859              core_course_external::get_courses_by_field('id', $newcourse->id)
2860          );
2861  
2862          $this->assertCount(1, $result['courses']);
2863          $course = reset($result['courses']);
2864  
2865          $this->assertArrayHasKey('customfields', $course);
2866          $this->assertCount(1, $course['customfields']);
2867  
2868          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
2869          $this->assertEquals([
2870              'name' => $datefield->get('name'),
2871              'shortname' => $datefield->get('shortname'),
2872              'type' => $datefield->get('type'),
2873              'value' => userdate(1580389200),
2874              'valueraw' => 1580389200,
2875          ], reset($course['customfields']));
2876      }
2877  
2878      public function test_get_courses_by_field_invalid_field() {
2879          $this->expectException('invalid_parameter_exception');
2880          $result = core_course_external::get_courses_by_field('zyx', 'x');
2881      }
2882  
2883      public function test_get_courses_by_field_invalid_courses() {
2884          $result = core_course_external::get_courses_by_field('id', '-1');
2885          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2886          $this->assertCount(0, $result['courses']);
2887      }
2888  
2889      /**
2890       * Test get_courses_by_field_invalid_theme_and_lang
2891       */
2892      public function test_get_courses_by_field_invalid_theme_and_lang() {
2893          $this->resetAfterTest(true);
2894          $this->setAdminUser();
2895  
2896          $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
2897          $result = core_course_external::get_courses_by_field('id', $course->id);
2898          $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2899          $this->assertEmpty($result['courses']['0']['theme']);
2900          $this->assertEmpty($result['courses']['0']['lang']);
2901      }
2902  
2903  
2904      public function test_check_updates() {
2905          global $DB;
2906          $this->resetAfterTest(true);
2907          $this->setAdminUser();
2908  
2909          // Create different types of activities.
2910          $course  = self::getDataGenerator()->create_course();
2911          $tocreate = array('assign', 'book', 'choice', 'folder', 'forum', 'glossary', 'imscp', 'label', 'lti', 'page', 'quiz',
2912                              'resource', 'scorm', 'survey', 'url', 'wiki');
2913  
2914          $modules = array();
2915          foreach ($tocreate as $modname) {
2916              $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
2917              $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
2918              $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
2919          }
2920  
2921          $student = self::getDataGenerator()->create_user();
2922          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2923          self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
2924          $this->setUser($student);
2925  
2926          $since = time();
2927          $this->waitForSecond();
2928          $params = array();
2929          foreach ($modules as $modname => $data) {
2930              $params[$data['cm']->id] = array(
2931                  'contextlevel' => 'module',
2932                  'id' => $data['cm']->id,
2933                  'since' => $since
2934              );
2935          }
2936  
2937          // Check there is nothing updated because modules are fresh new.
2938          $result = core_course_external::check_updates($course->id, $params);
2939          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2940          $this->assertCount(0, $result['instances']);
2941          $this->assertCount(0, $result['warnings']);
2942  
2943          // Test with get_updates_since the same data.
2944          $result = core_course_external::get_updates_since($course->id, $since);
2945          $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
2946          $this->assertCount(0, $result['instances']);
2947          $this->assertCount(0, $result['warnings']);
2948  
2949          // Update a module after a second.
2950          $this->waitForSecond();
2951          set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
2952  
2953          $found = false;
2954          $result = core_course_external::check_updates($course->id, $params);
2955          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2956          $this->assertCount(1, $result['instances']);
2957          $this->assertCount(0, $result['warnings']);
2958          foreach ($result['instances'] as $module) {
2959              foreach ($module['updates'] as $update) {
2960                  if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
2961                      $found = true;
2962                  }
2963              }
2964          }
2965          $this->assertTrue($found);
2966  
2967          // Test with get_updates_since the same data.
2968          $result = core_course_external::get_updates_since($course->id, $since);
2969          $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
2970          $this->assertCount(1, $result['instances']);
2971          $this->assertCount(0, $result['warnings']);
2972          $found = false;
2973          $this->assertCount(1, $result['instances']);
2974          $this->assertCount(0, $result['warnings']);
2975          foreach ($result['instances'] as $module) {
2976              foreach ($module['updates'] as $update) {
2977                  if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
2978                      $found = true;
2979                  }
2980              }
2981          }
2982          $this->assertTrue($found);
2983  
2984          // Do not retrieve the configuration field.
2985          $filter = array('files');
2986          $found = false;
2987          $result = core_course_external::check_updates($course->id, $params, $filter);
2988          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
2989          $this->assertCount(0, $result['instances']);
2990          $this->assertCount(0, $result['warnings']);
2991          $this->assertFalse($found);
2992  
2993          // Add invalid cmid.
2994          $params[] = array(
2995              'contextlevel' => 'module',
2996              'id' => -2,
2997              'since' => $since
2998          );
2999          $result = core_course_external::check_updates($course->id, $params);
3000          $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3001          $this->assertCount(1, $result['warnings']);
3002          $this->assertEquals(-2, $result['warnings'][0]['itemid']);
3003      }
3004  
3005      /**
3006       * Test cases for the get_enrolled_courses_by_timeline_classification test.
3007       */
3008      public function get_get_enrolled_courses_by_timeline_classification_test_cases():array {
3009          $now = time();
3010          $day = 86400;
3011  
3012          $coursedata = [
3013              [
3014                  'shortname' => 'apast',
3015                  'startdate' => $now - ($day * 2),
3016                  'enddate' => $now - $day
3017              ],
3018              [
3019                  'shortname' => 'bpast',
3020                  'startdate' => $now - ($day * 2),
3021                  'enddate' => $now - $day
3022              ],
3023              [
3024                  'shortname' => 'cpast',
3025                  'startdate' => $now - ($day * 2),
3026                  'enddate' => $now - $day
3027              ],
3028              [
3029                  'shortname' => 'dpast',
3030                  'startdate' => $now - ($day * 2),
3031                  'enddate' => $now - $day
3032              ],
3033              [
3034                  'shortname' => 'epast',
3035                  'startdate' => $now - ($day * 2),
3036                  'enddate' => $now - $day
3037              ],
3038              [
3039                  'shortname' => 'ainprogress',
3040                  'startdate' => $now - $day,
3041                  'enddate' => $now + $day
3042              ],
3043              [
3044                  'shortname' => 'binprogress',
3045                  'startdate' => $now - $day,
3046                  'enddate' => $now + $day
3047              ],
3048              [
3049                  'shortname' => 'cinprogress',
3050                  'startdate' => $now - $day,
3051                  'enddate' => $now + $day
3052              ],
3053              [
3054                  'shortname' => 'dinprogress',
3055                  'startdate' => $now - $day,
3056                  'enddate' => $now + $day
3057              ],
3058              [
3059                  'shortname' => 'einprogress',
3060                  'startdate' => $now - $day,
3061                  'enddate' => $now + $day
3062              ],
3063              [
3064                  'shortname' => 'afuture',
3065                  'startdate' => $now + $day
3066              ],
3067              [
3068                  'shortname' => 'bfuture',
3069                  'startdate' => $now + $day
3070              ],
3071              [
3072                  'shortname' => 'cfuture',
3073                  'startdate' => $now + $day
3074              ],
3075              [
3076                  'shortname' => 'dfuture',
3077                  'startdate' => $now + $day
3078              ],
3079              [
3080                  'shortname' => 'efuture',
3081                  'startdate' => $now + $day
3082              ]
3083          ];
3084  
3085          // Raw enrolled courses result set should be returned in this order:
3086          // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
3087          // dfuture, dinprogress, dpast, efuture, einprogress, epast
3088          //
3089          // By classification the offset values for each record should be:
3090          // COURSE_TIMELINE_FUTURE
3091          // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
3092          // COURSE_TIMELINE_INPROGRESS
3093          // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
3094          // COURSE_TIMELINE_PAST
3095          // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
3096          //
3097          // NOTE: The offset applies to the unfiltered full set of courses before the classification
3098          // filtering is done.
3099          // E.g. In our example if an offset of 2 is given then it would mean the first
3100          // two courses (afuture, ainprogress) are ignored.
3101          return [
3102              'empty set' => [
3103                  'coursedata' => [],
3104                  'classification' => 'future',
3105                  'limit' => 2,
3106                  'offset' => 0,
3107                  'sort' => 'shortname ASC',
3108                  'expectedcourses' => [],
3109                  'expectednextoffset' => 0
3110              ],
3111              // COURSE_TIMELINE_FUTURE.
3112              'future not limit no offset' => [
3113                  'coursedata' => $coursedata,
3114                  'classification' => 'future',
3115                  'limit' => 0,
3116                  'offset' => 0,
3117                  'sort' => 'shortname ASC',
3118                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3119                  'expectednextoffset' => 15
3120              ],
3121              'future no offset' => [
3122                  'coursedata' => $coursedata,
3123                  'classification' => 'future',
3124                  'limit' => 2,
3125                  'offset' => 0,
3126                  'sort' => 'shortname ASC',
3127                  'expectedcourses' => ['afuture', 'bfuture'],
3128                  'expectednextoffset' => 4
3129              ],
3130              'future offset' => [
3131                  'coursedata' => $coursedata,
3132                  'classification' => 'future',
3133                  'limit' => 2,
3134                  'offset' => 2,
3135                  'sort' => 'shortname ASC',
3136                  'expectedcourses' => ['bfuture', 'cfuture'],
3137                  'expectednextoffset' => 7
3138              ],
3139              'future exact limit' => [
3140                  'coursedata' => $coursedata,
3141                  'classification' => 'future',
3142                  'limit' => 5,
3143                  'offset' => 0,
3144                  'sort' => 'shortname ASC',
3145                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3146                  'expectednextoffset' => 13
3147              ],
3148              'future limit less results' => [
3149                  'coursedata' => $coursedata,
3150                  'classification' => 'future',
3151                  'limit' => 10,
3152                  'offset' => 0,
3153                  'sort' => 'shortname ASC',
3154                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3155                  'expectednextoffset' => 15
3156              ],
3157              'future limit less results with offset' => [
3158                  'coursedata' => $coursedata,
3159                  'classification' => 'future',
3160                  'limit' => 10,
3161                  'offset' => 5,
3162                  'sort' => 'shortname ASC',
3163                  'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
3164                  'expectednextoffset' => 15
3165              ],
3166              'all no limit or offset' => [
3167                  'coursedata' => $coursedata,
3168                  'classification' => 'all',
3169                  'limit' => 0,
3170                  'offset' => 0,
3171                  'sort' => 'shortname ASC',
3172                  'expectedcourses' => [
3173                      'afuture',
3174                      'ainprogress',
3175                      'apast',
3176                      'bfuture',
3177                      'binprogress',
3178                      'bpast',
3179                      'cfuture',
3180                      'cinprogress',
3181                      'cpast',
3182                      'dfuture',
3183                      'dinprogress',
3184                      'dpast',
3185                      'efuture',
3186                      'einprogress',
3187                      'epast'
3188                  ],
3189                  'expectednextoffset' => 15
3190              ],
3191              'all limit no offset' => [
3192                  'coursedata' => $coursedata,
3193                  'classification' => 'all',
3194                  'limit' => 5,
3195                  'offset' => 0,
3196                  'sort' => 'shortname ASC',
3197                  'expectedcourses' => [
3198                      'afuture',
3199                      'ainprogress',
3200                      'apast',
3201                      'bfuture',
3202                      'binprogress'
3203                  ],
3204                  'expectednextoffset' => 5
3205              ],
3206              'all limit and offset' => [
3207                  'coursedata' => $coursedata,
3208                  'classification' => 'all',
3209                  'limit' => 5,
3210                  'offset' => 5,
3211                  'sort' => 'shortname ASC',
3212                  'expectedcourses' => [
3213                      'bpast',
3214                      'cfuture',
3215                      'cinprogress',
3216                      'cpast',
3217                      'dfuture'
3218                  ],
3219                  'expectednextoffset' => 10
3220              ],
3221              'all offset past result set' => [
3222                  'coursedata' => $coursedata,
3223                  'classification' => 'all',
3224                  'limit' => 5,
3225                  'offset' => 50,
3226                  'sort' => 'shortname ASC',
3227                  'expectedcourses' => [],
3228                  'expectednextoffset' => 50
3229              ],
3230              'all limit and offset with sort ul.timeaccess desc' => [
3231                  'coursedata' => $coursedata,
3232                  'classification' => 'inprogress',
3233                  'limit' => 0,
3234                  'offset' => 0,
3235                  'sort' => 'ul.timeaccess desc',
3236                  'expectedcourses' => [
3237                      'ainprogress',
3238                      'binprogress',
3239                      'cinprogress',
3240                      'dinprogress',
3241                      'einprogress'
3242                  ],
3243                  'expectednextoffset' => 15
3244              ],
3245              'all limit and offset with sort sql injection for sort or 1==1' => [
3246                  'coursedata' => $coursedata,
3247                  'classification' => 'all',
3248                  'limit' => 5,
3249                  'offset' => 5,
3250                  'sort' => 'ul.timeaccess desc or 1==1',
3251                  'expectedcourses' => [],
3252                  'expectednextoffset' => 0,
3253                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3254              ],
3255              'all limit and offset with sql injection of sort a custom one' => [
3256                  'coursedata' => $coursedata,
3257                  'classification' => 'all',
3258                  'limit' => 5,
3259                  'offset' => 5,
3260                  'sort' => "ul.timeaccess LIMIT 1--",
3261                  'expectedcourses' => [],
3262                  'expectednextoffset' => 0,
3263                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3264              ],
3265              'all limit and offset with wrong sort direction' => [
3266                  'coursedata' => $coursedata,
3267                  'classification' => 'all',
3268                  'limit' => 5,
3269                  'offset' => 5,
3270                  'sort' => "ul.timeaccess abcdasc",
3271                  'expectedcourses' => [],
3272                  'expectednextoffset' => 0,
3273                  'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()'
3274              ],
3275              'all limit and offset with wrong sort direction' => [
3276                  'coursedata' => $coursedata,
3277                  'classification' => 'all',
3278                  'limit' => 5,
3279                  'offset' => 5,
3280                  'sort' => "ul.timeaccess.foo ascd",
3281                  'expectedcourses' => [],
3282                  'expectednextoffset' => 0,
3283                  'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()'
3284              ],
3285              'all limit and offset with wrong sort param' => [
3286                  'coursedata' => $coursedata,
3287                  'classification' => 'all',
3288                  'limit' => 5,
3289                  'offset' => 5,
3290                  'sort' => "foobar",
3291                  'expectedcourses' => [],
3292                  'expectednextoffset' => 0,
3293                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3294              ],
3295              'all limit and offset with wrong field name' => [
3296                  'coursedata' => $coursedata,
3297                  'classification' => 'all',
3298                  'limit' => 5,
3299                  'offset' => 5,
3300                  'sort' => "ul.foobar",
3301                  'expectedcourses' => [],
3302                  'expectednextoffset' => 0,
3303                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3304              ],
3305              'all limit and offset with wrong field separator' => [
3306                  'coursedata' => $coursedata,
3307                  'classification' => 'all',
3308                  'limit' => 5,
3309                  'offset' => 5,
3310                  'sort' => "ul.timeaccess.foo",
3311                  'expectedcourses' => [],
3312                  'expectednextoffset' => 0,
3313                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3314              ],
3315              'all limit and offset with wrong field separator #' => [
3316                  'coursedata' => $coursedata,
3317                  'classification' => 'all',
3318                  'limit' => 5,
3319                  'offset' => 5,
3320                  'sort' => "ul#timeaccess",
3321                  'expectedcourses' => [],
3322                  'expectednextoffset' => 0,
3323                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3324              ],
3325              'all limit and offset with wrong field separator $' => [
3326                  'coursedata' => $coursedata,
3327                  'classification' => 'all',
3328                  'limit' => 5,
3329                  'offset' => 5,
3330                  'sort' => 'ul$timeaccess',
3331                  'expectedcourses' => [],
3332                  'expectednextoffset' => 0,
3333                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3334              ],
3335              'all limit and offset with wrong field name' => [
3336                  'coursedata' => $coursedata,
3337                  'classification' => 'all',
3338                  'limit' => 5,
3339                  'offset' => 5,
3340                  'sort' => 'timeaccess123',
3341                  'expectedcourses' => [],
3342                  'expectednextoffset' => 0,
3343                  'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
3344              ],
3345              'all limit and offset with no sort direction for ul' => [
3346                  'coursedata' => $coursedata,
3347                  'classification' => 'inprogress',
3348                  'limit' => 0,
3349                  'offset' => 0,
3350                  'sort' => "ul.timeaccess",
3351                  'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3352                  'expectednextoffset' => 15,
3353              ],
3354              'all limit and offset with valid field name and no prefix, test for ul' => [
3355                  'coursedata' => $coursedata,
3356                  'classification' => 'inprogress',
3357                  'limit' => 0,
3358                  'offset' => 0,
3359                  'sort' => "timeaccess",
3360                  'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3361                  'expectednextoffset' => 15,
3362              ],
3363              'all limit and offset with valid field name and no prefix' => [
3364                  'coursedata' => $coursedata,
3365                  'classification' => 'all',
3366                  'limit' => 5,
3367                  'offset' => 5,
3368                  'sort' => "fullname",
3369                  'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3370                  'expectednextoffset' => 10,
3371              ],
3372              'all limit and offset with valid field name and no prefix and with sort direction' => [
3373                  'coursedata' => $coursedata,
3374                  'classification' => 'all',
3375                  'limit' => 5,
3376                  'offset' => 5,
3377                  'sort' => "fullname desc",
3378                  'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3379                  'expectednextoffset' => 10,
3380              ],
3381          ];
3382      }
3383  
3384      /**
3385       * Test the get_enrolled_courses_by_timeline_classification function.
3386       *
3387       * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
3388       * @param array $coursedata Courses to create
3389       * @param string $classification Timeline classification
3390       * @param int $limit Maximum number of results
3391       * @param int $offset Offset the unfiltered courses result set by this amount
3392       * @param string $sort sort the courses
3393       * @param array $expectedcourses Expected courses in result
3394       * @param int $expectednextoffset Expected next offset value in result
3395       * @param string|null $expectedexception Expected exception string
3396       */
3397      public function test_get_enrolled_courses_by_timeline_classification(
3398          $coursedata,
3399          $classification,
3400          $limit,
3401          $offset,
3402          $sort,
3403          $expectedcourses,
3404          $expectednextoffset,
3405          $expectedexception = null
3406      ) {
3407          $this->resetAfterTest();
3408          $generator = $this->getDataGenerator();
3409  
3410          $courses = array_map(function($coursedata) use ($generator) {
3411              return $generator->create_course($coursedata);
3412          }, $coursedata);
3413  
3414          $student = $generator->create_user();
3415  
3416          foreach ($courses as $course) {
3417              $generator->enrol_user($student->id, $course->id, 'student');
3418          }
3419  
3420          $this->setUser($student);
3421  
3422          if (isset($expectedexception)) {
3423              $this->expectException('coding_exception');
3424              $this->expectExceptionMessage($expectedexception);
3425          }
3426  
3427          // NOTE: The offset applies to the unfiltered full set of courses before the classification
3428          // filtering is done.
3429          // E.g. In our example if an offset of 2 is given then it would mean the first
3430          // two courses (afuture, ainprogress) are ignored.
3431          $result = core_course_external::get_enrolled_courses_by_timeline_classification(
3432              $classification,
3433              $limit,
3434              $offset,
3435              $sort
3436          );
3437          $result = external_api::clean_returnvalue(
3438              core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
3439              $result
3440          );
3441  
3442          $actual = array_map(function($course) {
3443              return $course['shortname'];
3444          }, $result['courses']);
3445  
3446          $this->assertEqualsCanonicalizing($expectedcourses, $actual);
3447          $this->assertEquals($expectednextoffset, $result['nextoffset']);
3448      }
3449  
3450      /**
3451       * Test the get_recent_courses function.
3452       */
3453      public function test_get_recent_courses() {
3454          global $USER, $DB;
3455  
3456          $this->resetAfterTest();
3457          $generator = $this->getDataGenerator();
3458  
3459          set_config('hiddenuserfields', 'lastaccess');
3460  
3461          $courses = array();
3462          for ($i = 1; $i < 12; $i++) {
3463              $courses[]  = $generator->create_course();
3464          };
3465  
3466          $student = $generator->create_user();
3467          $teacher = $generator->create_user();
3468  
3469          foreach ($courses as $course) {
3470              $generator->enrol_user($student->id, $course->id, 'student');
3471          }
3472  
3473          $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher');
3474  
3475          $this->setUser($student);
3476  
3477          $result = core_course_external::get_recent_courses($USER->id);
3478  
3479          // No course accessed.
3480          $this->assertCount(0, $result);
3481  
3482          foreach ($courses as $course) {
3483              core_course_external::view_course($course->id);
3484          }
3485  
3486          // Every course accessed.
3487          $result = core_course_external::get_recent_courses($USER->id);
3488          $this->assertCount( 11, $result);
3489  
3490          // Every course accessed, result limited to 10 courses.
3491          $result = core_course_external::get_recent_courses($USER->id, 10);
3492          $this->assertCount(10, $result);
3493  
3494          $guestcourse = $generator->create_course(
3495                  (object)array('shortname' => 'guestcourse',
3496                  'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
3497                  'enrol_guest_password_0' => ''));
3498          core_course_external::view_course($guestcourse->id);
3499  
3500          // Every course accessed, even the not enrolled one.
3501          $result = core_course_external::get_recent_courses($USER->id);
3502          $this->assertCount(12, $result);
3503  
3504          // Offset 5, return 7 out of 12.
3505          $result = core_course_external::get_recent_courses($USER->id, 0, 5);
3506          $this->assertCount(7, $result);
3507  
3508          // Offset 5 and limit 3, return 3 out of 12.
3509          $result = core_course_external::get_recent_courses($USER->id, 3, 5);
3510          $this->assertCount(3, $result);
3511  
3512          // Sorted by course id ASC.
3513          $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC');
3514          $this->assertEquals($courses[0]->id, array_shift($result)->id);
3515  
3516          // Sorted by course id DESC.
3517          $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC');
3518          $this->assertEquals($guestcourse->id, array_shift($result)->id);
3519  
3520          // If last access is hidden, only get the courses where has viewhiddenuserfields capability.
3521          $this->setUser($teacher);
3522          $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
3523          $usercontext = context_user::instance($student->id);
3524          $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
3525  
3526          // Sorted by course id DESC.
3527          $result = core_course_external::get_recent_courses($student->id);
3528          $this->assertCount(1, $result);
3529          $this->assertEquals($courses[0]->id, array_shift($result)->id);
3530      }
3531  
3532      /**
3533       * Test get enrolled users by cmid function.
3534       */
3535      public function test_get_enrolled_users_by_cmid() {
3536          global $PAGE;
3537          $this->resetAfterTest(true);
3538  
3539          $user1 = self::getDataGenerator()->create_user();
3540          $user2 = self::getDataGenerator()->create_user();
3541          $user3 = self::getDataGenerator()->create_user();
3542  
3543          $user1picture = new user_picture($user1);
3544          $user1picture->size = 1;
3545          $user1->profileimage = $user1picture->get_url($PAGE)->out(false);
3546  
3547          $user2picture = new user_picture($user2);
3548          $user2picture->size = 1;
3549          $user2->profileimage = $user2picture->get_url($PAGE)->out(false);
3550  
3551          $user3picture = new user_picture($user3);
3552          $user3picture->size = 1;
3553          $user3->profileimage = $user3picture->get_url($PAGE)->out(false);
3554  
3555          // Set the first created user to the test user.
3556          self::setUser($user1);
3557  
3558          // Create course to add the module.
3559          $course1 = self::getDataGenerator()->create_course();
3560  
3561          // Forum with tracking off.
3562          $record = new stdClass();
3563          $record->course = $course1->id;
3564          $forum1 = self::getDataGenerator()->create_module('forum', $record);
3565  
3566          // Following lines enrol and assign default role id to the users.
3567          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
3568          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
3569          // Enrol a suspended user in the course.
3570          $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
3571  
3572          // Create what we expect to be returned when querying the course module.
3573          $expectedusers = array(
3574              'users' => array(),
3575              'warnings' => array(),
3576          );
3577  
3578          $expectedusers['users'][0] = [
3579              'id' => $user1->id,
3580              'fullname' => fullname($user1),
3581              'firstname' => $user1->firstname,
3582              'lastname' => $user1->lastname,
3583              'profileimage' => $user1->profileimage,
3584          ];
3585          $expectedusers['users'][1] = [
3586              'id' => $user2->id,
3587              'fullname' => fullname($user2),
3588              'firstname' => $user2->firstname,
3589              'lastname' => $user2->lastname,
3590              'profileimage' => $user2->profileimage,
3591          ];
3592          $expectedusers['users'][2] = [
3593              'id' => $user3->id,
3594              'fullname' => fullname($user3),
3595              'firstname' => $user3->firstname,
3596              'lastname' => $user3->lastname,
3597              'profileimage' => $user3->profileimage,
3598          ];
3599  
3600          // Test getting the users in a given context.
3601          $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
3602          $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3603  
3604          $this->assertEquals(3, count($users['users']));
3605          $this->assertEquals($expectedusers, $users);
3606  
3607          // Test getting only the active users in a given context.
3608          $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true);
3609          $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3610  
3611          $expectedusers['users'] = [
3612              [
3613                  'id' => $user1->id,
3614                  'fullname' => fullname($user1),
3615                  'firstname' => $user1->firstname,
3616                  'lastname' => $user1->lastname,
3617                  'profileimage' => $user1->profileimage,
3618              ],
3619              [
3620                  'id' => $user2->id,
3621                  'fullname' => fullname($user2),
3622                  'firstname' => $user2->firstname,
3623                  'lastname' => $user2->lastname,
3624                  'profileimage' => $user2->profileimage,
3625              ]
3626          ];
3627  
3628          $this->assertEquals(2, count($users['users']));
3629          $this->assertEquals($expectedusers, $users);
3630      }
3631  
3632      /**
3633       * Verify that content items can be added to user favourites.
3634       */
3635      public function test_add_content_item_to_user_favourites() {
3636          $this->resetAfterTest();
3637  
3638          $course = $this->getDataGenerator()->create_course();
3639          $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
3640          $this->setUser($user);
3641  
3642          // Using the internal API, confirm that no items are set as favourites for the user.
3643          $contentitemservice = new \core_course\local\service\content_item_service(
3644              new \core_course\local\repository\content_item_readonly_repository()
3645          );
3646          $contentitems = $contentitemservice->get_all_content_items($user);
3647          $favourited = array_filter($contentitems, function($contentitem) {
3648              return $contentitem->favourite == true;
3649          });
3650          $this->assertCount(0, $favourited);
3651  
3652          // Using the external API, favourite a content item for the user.
3653          $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
3654          $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id);
3655          $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(),
3656              $contentitem);
3657  
3658          // Verify the returned item is a favourite.
3659          $this->assertTrue($contentitem['favourite']);
3660  
3661          // Using the internal API, confirm we see a single favourite item.
3662          $contentitems = $contentitemservice->get_all_content_items($user);
3663          $favourited = array_values(array_filter($contentitems, function($contentitem) {
3664              return $contentitem->favourite == true;
3665          }));
3666          $this->assertCount(1, $favourited);
3667          $this->assertEquals('assign', $favourited[0]->name);
3668      }
3669  
3670      /**
3671       * Verify that content items can be removed from user favourites.
3672       */
3673      public function test_remove_content_item_from_user_favourites() {
3674          $this->resetAfterTest();
3675  
3676          $course = $this->getDataGenerator()->create_course();
3677          $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
3678          $this->setUser($user);
3679  
3680          // Using the internal API, set a favourite for the user.
3681          $contentitemservice = new \core_course\local\service\content_item_service(
3682              new \core_course\local\repository\content_item_readonly_repository()
3683          );
3684          $contentitems = $contentitemservice->get_all_content_items($user);
3685          $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
3686          $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id);
3687  
3688          $contentitems = $contentitemservice->get_all_content_items($user);
3689          $favourited = array_filter($contentitems, function($contentitem) {
3690              return $contentitem->favourite == true;
3691          });
3692          $this->assertCount(1, $favourited);
3693  
3694          // Now, verify the external API can remove the favourite.
3695          $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id);
3696          $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(),
3697              $contentitem);
3698  
3699          // Verify the returned item is a favourite.
3700          $this->assertFalse($contentitem['favourite']);
3701  
3702          // Using the internal API, confirm we see no favourite items.
3703          $contentitems = $contentitemservice->get_all_content_items($user);
3704          $favourited = array_filter($contentitems, function($contentitem) {
3705              return $contentitem->favourite == true;
3706          });
3707          $this->assertCount(0, $favourited);
3708      }
3709  
3710      /**
3711       * Test the web service returning course content items for inclusion in activity choosers, etc.
3712       */
3713      public function test_get_course_content_items() {
3714          $this->resetAfterTest();
3715  
3716          $course  = self::getDataGenerator()->create_course();
3717          $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
3718  
3719          // Fetch available content items as the editing teacher.
3720          $this->setUser($user);
3721          $result = core_course_external::get_course_content_items($course->id);
3722          $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
3723  
3724          $contentitemservice = new \core_course\local\service\content_item_service(
3725              new \core_course\local\repository\content_item_readonly_repository()
3726          );
3727  
3728          // Check if the webservice returns exactly what the service defines, albeit in array form.
3729          $serviceitemsasarray = array_map(function($item) {
3730              return (array) $item;
3731          }, $contentitemservice->get_content_items_for_user_in_course($user, $course));
3732  
3733          $this->assertEquals($serviceitemsasarray, $result['content_items']);
3734      }
3735  
3736      /**
3737       * Test the web service returning course content items, specifically in case where the user can't manage activities.
3738       */
3739      public function test_get_course_content_items_no_permission_to_manage() {
3740          $this->resetAfterTest();
3741  
3742          $course  = self::getDataGenerator()->create_course();
3743          $user = self::getDataGenerator()->create_and_enrol($course, 'student');
3744  
3745          // Fetch available content items as a student, who won't have the permission to manage activities.
3746          $this->setUser($user);
3747          $result = core_course_external::get_course_content_items($course->id);
3748          $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
3749  
3750          $this->assertEmpty($result['content_items']);
3751      }
3752  
3753      /**
3754       * Test toggling the recommendation of an activity.
3755       */
3756      public function test_toggle_activity_recommendation() {
3757          global $CFG;
3758  
3759          $this->resetAfterTest();
3760  
3761          $context = context_system::instance();
3762          $usercontext = context_user::instance($CFG->siteguest);
3763          $component = 'core_course';
3764          $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
3765  
3766          $areaname = 'test_core';
3767          $areaid = 3;
3768  
3769          // Test we have the favourite.
3770          $this->setAdminUser();
3771          $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
3772          $this->assertTrue($favouritefactory->favourite_exists($component,
3773                  \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context));
3774          $this->assertTrue($result['status']);
3775          // Test that it is now gone.
3776          $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
3777          $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context));
3778          $this->assertFalse($result['status']);
3779      }
3780  }