Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

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