Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   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   * SCORM module external functions tests
  19   *
  20   * @package    mod_scorm
  21   * @category   external
  22   * @copyright  2015 Juan Leyva <juan@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @since      Moodle 3.0
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  global $CFG;
  30  
  31  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  32  require_once($CFG->dirroot . '/mod/scorm/lib.php');
  33  
  34  /**
  35   * SCORM module external functions tests
  36   *
  37   * @package    mod_scorm
  38   * @category   external
  39   * @copyright  2015 Juan Leyva <juan@moodle.com>
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   * @since      Moodle 3.0
  42   */
  43  class mod_scorm_external_testcase extends externallib_advanced_testcase {
  44  
  45      /**
  46       * Set up for every test
  47       */
  48      public function setUp(): void {
  49          global $DB, $CFG;
  50          $this->resetAfterTest();
  51          $this->setAdminUser();
  52  
  53          $CFG->enablecompletion = 1;
  54          // Setup test data.
  55          $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
  56          $this->scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $this->course->id),
  57              array('completion' => 2, 'completionview' => 1));
  58          $this->context = context_module::instance($this->scorm->cmid);
  59          $this->cm = get_coursemodule_from_instance('scorm', $this->scorm->id);
  60  
  61          // Create users.
  62          $this->student = self::getDataGenerator()->create_user();
  63          $this->teacher = self::getDataGenerator()->create_user();
  64  
  65          // Users enrolments.
  66          $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
  67          $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
  68          $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
  69          $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
  70      }
  71  
  72      /**
  73       * Test view_scorm
  74       */
  75      public function test_view_scorm() {
  76          global $DB;
  77  
  78          // Test invalid instance id.
  79          try {
  80              mod_scorm_external::view_scorm(0);
  81              $this->fail('Exception expected due to invalid mod_scorm instance id.');
  82          } catch (moodle_exception $e) {
  83              $this->assertEquals('invalidrecord', $e->errorcode);
  84          }
  85  
  86          // Test not-enrolled user.
  87          $user = self::getDataGenerator()->create_user();
  88          $this->setUser($user);
  89          try {
  90              mod_scorm_external::view_scorm($this->scorm->id);
  91              $this->fail('Exception expected due to not enrolled user.');
  92          } catch (moodle_exception $e) {
  93              $this->assertEquals('requireloginerror', $e->errorcode);
  94          }
  95  
  96          // Test user with full capabilities.
  97          $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
  98          $this->getDataGenerator()->enrol_user($user->id, $this->course->id, $this->studentrole->id);
  99  
 100          // Trigger and capture the event.
 101          $sink = $this->redirectEvents();
 102  
 103          $result = mod_scorm_external::view_scorm($this->scorm->id);
 104          $result = external_api::clean_returnvalue(mod_scorm_external::view_scorm_returns(), $result);
 105  
 106          $events = $sink->get_events();
 107          $this->assertCount(1, $events);
 108          $event = array_shift($events);
 109  
 110          // Checking that the event contains the expected values.
 111          $this->assertInstanceOf('\mod_scorm\event\course_module_viewed', $event);
 112          $this->assertEquals($this->context, $event->get_context());
 113          $moodleurl = new \moodle_url('/mod/scorm/view.php', array('id' => $this->cm->id));
 114          $this->assertEquals($moodleurl, $event->get_url());
 115          $this->assertEventContextNotUsed($event);
 116          $this->assertNotEmpty($event->get_name());
 117      }
 118  
 119      /**
 120       * Test get scorm attempt count
 121       */
 122      public function test_mod_scorm_get_scorm_attempt_count_own_empty() {
 123          // Set to the student user.
 124          self::setUser($this->student);
 125  
 126          // Retrieve my attempts (should be 0).
 127          $result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
 128          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
 129          $this->assertEquals(0, $result['attemptscount']);
 130      }
 131  
 132      public function test_mod_scorm_get_scorm_attempt_count_own_with_complete() {
 133          // Set to the student user.
 134          self::setUser($this->student);
 135  
 136          // Create attempts.
 137          $scoes = scorm_get_scoes($this->scorm->id);
 138          $sco = array_shift($scoes);
 139          scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
 140          scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 2, 'cmi.core.lesson_status', 'completed');
 141  
 142          $result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
 143          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
 144          $this->assertEquals(2, $result['attemptscount']);
 145      }
 146  
 147      public function test_mod_scorm_get_scorm_attempt_count_own_incomplete() {
 148          // Set to the student user.
 149          self::setUser($this->student);
 150  
 151          // Create a complete attempt, and an incomplete attempt.
 152          $scoes = scorm_get_scoes($this->scorm->id);
 153          $sco = array_shift($scoes);
 154          scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
 155          scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 2, 'cmi.core.credit', '0');
 156  
 157          $result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id, true);
 158          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
 159          $this->assertEquals(1, $result['attemptscount']);
 160      }
 161  
 162      public function test_mod_scorm_get_scorm_attempt_count_others_as_teacher() {
 163          // As a teacher.
 164          self::setUser($this->teacher);
 165  
 166          // Create a completed attempt for student.
 167          $scoes = scorm_get_scoes($this->scorm->id);
 168          $sco = array_shift($scoes);
 169          scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
 170  
 171          // I should be able to view the attempts for my students.
 172          $result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
 173          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
 174          $this->assertEquals(1, $result['attemptscount']);
 175      }
 176  
 177      public function test_mod_scorm_get_scorm_attempt_count_others_as_student() {
 178          // Create a second student.
 179          $student2 = self::getDataGenerator()->create_user();
 180          $this->getDataGenerator()->enrol_user($student2->id, $this->course->id, $this->studentrole->id, 'manual');
 181  
 182          // As a student.
 183          self::setUser($student2);
 184  
 185          // I should not be able to view the attempts of another student.
 186          $this->expectException(required_capability_exception::class);
 187          mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
 188      }
 189  
 190      public function test_mod_scorm_get_scorm_attempt_count_invalid_instanceid() {
 191          // As student.
 192          self::setUser($this->student);
 193  
 194          // Test invalid instance id.
 195          $this->expectException(moodle_exception::class);
 196          mod_scorm_external::get_scorm_attempt_count(0, $this->student->id);
 197      }
 198  
 199      public function test_mod_scorm_get_scorm_attempt_count_invalid_userid() {
 200          // As student.
 201          self::setUser($this->student);
 202  
 203          $this->expectException(moodle_exception::class);
 204          mod_scorm_external::get_scorm_attempt_count($this->scorm->id, -1);
 205      }
 206  
 207      /**
 208       * Test get scorm scoes
 209       */
 210      public function test_mod_scorm_get_scorm_scoes() {
 211          global $DB;
 212  
 213          $this->resetAfterTest(true);
 214  
 215          // Create users.
 216          $student = self::getDataGenerator()->create_user();
 217          $teacher = self::getDataGenerator()->create_user();
 218  
 219          // Create courses to add the modules.
 220          $course = self::getDataGenerator()->create_course();
 221  
 222          // First scorm, dates restriction.
 223          $record = new stdClass();
 224          $record->course = $course->id;
 225          $record->timeopen = time() + DAYSECS;
 226          $record->timeclose = $record->timeopen + DAYSECS;
 227          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 228  
 229          // Set to the student user.
 230          self::setUser($student);
 231  
 232          // Users enrolments.
 233          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 234          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
 235          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
 236          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
 237  
 238          // Retrieve my scoes, warning!.
 239          try {
 240               mod_scorm_external::get_scorm_scoes($scorm->id);
 241              $this->fail('Exception expected due to invalid dates.');
 242          } catch (moodle_exception $e) {
 243              $this->assertEquals('notopenyet', $e->errorcode);
 244          }
 245  
 246          $scorm->timeopen = time() - DAYSECS;
 247          $scorm->timeclose = time() - HOURSECS;
 248          $DB->update_record('scorm', $scorm);
 249  
 250          try {
 251               mod_scorm_external::get_scorm_scoes($scorm->id);
 252              $this->fail('Exception expected due to invalid dates.');
 253          } catch (moodle_exception $e) {
 254              $this->assertEquals('expired', $e->errorcode);
 255          }
 256  
 257          // Retrieve my scoes, user with permission.
 258          self::setUser($teacher);
 259          $result = mod_scorm_external::get_scorm_scoes($scorm->id);
 260          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
 261          $this->assertCount(2, $result['scoes']);
 262          $this->assertCount(0, $result['warnings']);
 263  
 264          $scoes = scorm_get_scoes($scorm->id);
 265          $sco = array_shift($scoes);
 266          $sco->extradata = array();
 267          $this->assertEquals((array) $sco, $result['scoes'][0]);
 268  
 269          $sco = array_shift($scoes);
 270          $sco->extradata = array();
 271          $sco->extradata[] = array(
 272              'element' => 'isvisible',
 273              'value' => $sco->isvisible
 274          );
 275          $sco->extradata[] = array(
 276              'element' => 'parameters',
 277              'value' => $sco->parameters
 278          );
 279          unset($sco->isvisible);
 280          unset($sco->parameters);
 281  
 282          // Sort the array (if we don't sort tests will fails for Postgres).
 283          usort($result['scoes'][1]['extradata'], function($a, $b) {
 284              return strcmp($a['element'], $b['element']);
 285          });
 286  
 287          $this->assertEquals((array) $sco, $result['scoes'][1]);
 288  
 289          // Use organization.
 290          $organization = 'golf_sample_default_org';
 291          $result = mod_scorm_external::get_scorm_scoes($scorm->id, $organization);
 292          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
 293          $this->assertCount(1, $result['scoes']);
 294          $this->assertEquals($organization, $result['scoes'][0]['organization']);
 295          $this->assertCount(0, $result['warnings']);
 296  
 297          // Test invalid instance id.
 298          try {
 299               mod_scorm_external::get_scorm_scoes(0);
 300              $this->fail('Exception expected due to invalid instance id.');
 301          } catch (moodle_exception $e) {
 302              $this->assertEquals('invalidrecord', $e->errorcode);
 303          }
 304  
 305      }
 306  
 307      /**
 308       * Test get scorm scoes (with a complex SCORM package)
 309       */
 310      public function test_mod_scorm_get_scorm_scoes_complex_package() {
 311          global $CFG;
 312  
 313          // As student.
 314          self::setUser($this->student);
 315  
 316          $record = new stdClass();
 317          $record->course = $this->course->id;
 318          $record->packagefilepath = $CFG->dirroot.'/mod/scorm/tests/packages/complexscorm.zip';
 319          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 320  
 321          $result = mod_scorm_external::get_scorm_scoes($scorm->id);
 322          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
 323          $this->assertCount(9, $result['scoes']);
 324          $this->assertCount(0, $result['warnings']);
 325  
 326          $expectedscoes = array();
 327          $scoreturnstructure = mod_scorm_external::get_scorm_scoes_returns();
 328          $scoes = scorm_get_scoes($scorm->id);
 329          foreach ($scoes as $sco) {
 330              $sco->extradata = array();
 331              foreach ($sco as $element => $value) {
 332                  // Add the extra data to the extradata array and remove the object element.
 333                  if (!isset($scoreturnstructure->keys['scoes']->content->keys[$element])) {
 334                      $sco->extradata[] = array(
 335                          'element' => $element,
 336                          'value' => $value
 337                      );
 338                      unset($sco->{$element});
 339                  }
 340              }
 341              $expectedscoes[] = (array) $sco;
 342          }
 343  
 344          $this->assertEquals($expectedscoes, $result['scoes']);
 345      }
 346  
 347      /*
 348       * Test get scorm user data
 349       */
 350      public function test_mod_scorm_get_scorm_user_data() {
 351          global $DB;
 352  
 353          $this->resetAfterTest(true);
 354  
 355          // Create users.
 356          $student1 = self::getDataGenerator()->create_user();
 357          $teacher = self::getDataGenerator()->create_user();
 358  
 359          // Set to the student user.
 360          self::setUser($student1);
 361  
 362          // Create courses to add the modules.
 363          $course = self::getDataGenerator()->create_course();
 364  
 365          // First scorm.
 366          $record = new stdClass();
 367          $record->course = $course->id;
 368          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 369  
 370          // Users enrolments.
 371          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 372          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
 373          $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id, 'manual');
 374          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
 375  
 376          // Create attempts.
 377          $scoes = scorm_get_scoes($scorm->id);
 378          $sco = array_shift($scoes);
 379          scorm_insert_track($student1->id, $scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
 380          scorm_insert_track($student1->id, $scorm->id, $sco->id, 1, 'cmi.core.score.raw', '80');
 381          scorm_insert_track($student1->id, $scorm->id, $sco->id, 2, 'cmi.core.lesson_status', 'completed');
 382  
 383          $result = mod_scorm_external::get_scorm_user_data($scorm->id, 1);
 384          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_user_data_returns(), $result);
 385          $this->assertCount(2, $result['data']);
 386          // Find our tracking data.
 387          $found = 0;
 388          foreach ($result['data'] as $scodata) {
 389              foreach ($scodata['userdata'] as $userdata) {
 390                  if ($userdata['element'] == 'cmi.core.lesson_status' and $userdata['value'] == 'completed') {
 391                      $found++;
 392                  }
 393                  if ($userdata['element'] == 'cmi.core.score.raw' and $userdata['value'] == '80') {
 394                      $found++;
 395                  }
 396              }
 397          }
 398          $this->assertEquals(2, $found);
 399  
 400          // Test invalid instance id.
 401          try {
 402               mod_scorm_external::get_scorm_user_data(0, 1);
 403              $this->fail('Exception expected due to invalid instance id.');
 404          } catch (moodle_exception $e) {
 405              $this->assertEquals('invalidrecord', $e->errorcode);
 406          }
 407      }
 408  
 409      /**
 410       * Test insert scorm tracks
 411       */
 412      public function test_mod_scorm_insert_scorm_tracks() {
 413          global $DB;
 414  
 415          $this->resetAfterTest(true);
 416  
 417          // Create users.
 418          $student = self::getDataGenerator()->create_user();
 419  
 420          // Create courses to add the modules.
 421          $course = self::getDataGenerator()->create_course();
 422  
 423          // First scorm, dates restriction.
 424          $record = new stdClass();
 425          $record->course = $course->id;
 426          $record->timeopen = time() + DAYSECS;
 427          $record->timeclose = $record->timeopen + DAYSECS;
 428          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 429  
 430          // Get a SCO.
 431          $scoes = scorm_get_scoes($scorm->id);
 432          $sco = array_shift($scoes);
 433  
 434          // Tracks.
 435          $tracks = array();
 436          $tracks[] = array(
 437              'element' => 'cmi.core.lesson_status',
 438              'value' => 'completed'
 439          );
 440          $tracks[] = array(
 441              'element' => 'cmi.core.score.raw',
 442              'value' => '80'
 443          );
 444  
 445          // Set to the student user.
 446          self::setUser($student);
 447  
 448          // Users enrolments.
 449          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 450          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
 451  
 452          // Exceptions first.
 453          try {
 454              mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks);
 455              $this->fail('Exception expected due to dates');
 456          } catch (moodle_exception $e) {
 457              $this->assertEquals('notopenyet', $e->errorcode);
 458          }
 459  
 460          $scorm->timeopen = time() - DAYSECS;
 461          $scorm->timeclose = time() - HOURSECS;
 462          $DB->update_record('scorm', $scorm);
 463  
 464          try {
 465              mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks);
 466              $this->fail('Exception expected due to dates');
 467          } catch (moodle_exception $e) {
 468              $this->assertEquals('expired', $e->errorcode);
 469          }
 470  
 471          // Test invalid instance id.
 472          try {
 473               mod_scorm_external::insert_scorm_tracks(0, 1, $tracks);
 474              $this->fail('Exception expected due to invalid sco id.');
 475          } catch (moodle_exception $e) {
 476              $this->assertEquals('cannotfindsco', $e->errorcode);
 477          }
 478  
 479          $scorm->timeopen = 0;
 480          $scorm->timeclose = 0;
 481          $DB->update_record('scorm', $scorm);
 482  
 483          // Retrieve my tracks.
 484          $result = mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks);
 485          $result = external_api::clean_returnvalue(mod_scorm_external::insert_scorm_tracks_returns(), $result);
 486          $this->assertCount(0, $result['warnings']);
 487  
 488          $trackids = $DB->get_records('scorm_scoes_track', array('userid' => $student->id, 'scoid' => $sco->id,
 489                                                                  'scormid' => $scorm->id, 'attempt' => 1));
 490          // We use asort here to prevent problems with ids ordering.
 491          $expectedkeys = array_keys($trackids);
 492          $this->assertEquals(asort($expectedkeys), asort($result['trackids']));
 493      }
 494  
 495      /**
 496       * Test get scorm sco tracks
 497       */
 498      public function test_mod_scorm_get_scorm_sco_tracks() {
 499          global $DB;
 500  
 501          $this->resetAfterTest(true);
 502  
 503          // Create users.
 504          $student = self::getDataGenerator()->create_user();
 505          $otherstudent = self::getDataGenerator()->create_user();
 506          $teacher = self::getDataGenerator()->create_user();
 507  
 508          // Set to the student user.
 509          self::setUser($student);
 510  
 511          // Create courses to add the modules.
 512          $course = self::getDataGenerator()->create_course();
 513  
 514          // First scorm.
 515          $record = new stdClass();
 516          $record->course = $course->id;
 517          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 518  
 519          // Users enrolments.
 520          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 521          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
 522          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
 523          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
 524  
 525          // Create attempts.
 526          $scoes = scorm_get_scoes($scorm->id);
 527          $sco = array_shift($scoes);
 528          scorm_insert_track($student->id, $scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
 529          scorm_insert_track($student->id, $scorm->id, $sco->id, 1, 'cmi.core.score.raw', '80');
 530          scorm_insert_track($student->id, $scorm->id, $sco->id, 2, 'cmi.core.lesson_status', 'completed');
 531  
 532          $result = mod_scorm_external::get_scorm_sco_tracks($sco->id, $student->id, 1);
 533          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_sco_tracks_returns(), $result);
 534          // 7 default elements + 2 custom ones.
 535          $this->assertCount(9, $result['data']['tracks']);
 536          $this->assertEquals(1, $result['data']['attempt']);
 537          $this->assertCount(0, $result['warnings']);
 538          // Find our tracking data.
 539          $found = 0;
 540          foreach ($result['data']['tracks'] as $userdata) {
 541              if ($userdata['element'] == 'cmi.core.lesson_status' and $userdata['value'] == 'completed') {
 542                  $found++;
 543              }
 544              if ($userdata['element'] == 'cmi.core.score.raw' and $userdata['value'] == '80') {
 545                  $found++;
 546              }
 547          }
 548          $this->assertEquals(2, $found);
 549  
 550          // Try invalid attempt.
 551          $result = mod_scorm_external::get_scorm_sco_tracks($sco->id, $student->id, 10);
 552          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_sco_tracks_returns(), $result);
 553          $this->assertCount(0, $result['data']['tracks']);
 554          $this->assertEquals(10, $result['data']['attempt']);
 555          $this->assertCount(1, $result['warnings']);
 556          $this->assertEquals('notattempted', $result['warnings'][0]['warningcode']);
 557  
 558          // Capabilities check.
 559          try {
 560               mod_scorm_external::get_scorm_sco_tracks($sco->id, $otherstudent->id);
 561              $this->fail('Exception expected due to invalid instance id.');
 562          } catch (required_capability_exception $e) {
 563              $this->assertEquals('nopermissions', $e->errorcode);
 564          }
 565  
 566          self::setUser($teacher);
 567          // Ommit the attempt parameter, the function should calculate the last attempt.
 568          $result = mod_scorm_external::get_scorm_sco_tracks($sco->id, $student->id);
 569          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_sco_tracks_returns(), $result);
 570          // 7 default elements + 1 custom one.
 571          $this->assertCount(8, $result['data']['tracks']);
 572          $this->assertEquals(2, $result['data']['attempt']);
 573  
 574          // Test invalid instance id.
 575          try {
 576               mod_scorm_external::get_scorm_sco_tracks(0, 1);
 577              $this->fail('Exception expected due to invalid instance id.');
 578          } catch (moodle_exception $e) {
 579              $this->assertEquals('cannotfindsco', $e->errorcode);
 580          }
 581          // Invalid user.
 582          try {
 583               mod_scorm_external::get_scorm_sco_tracks($sco->id, 0);
 584              $this->fail('Exception expected due to invalid instance id.');
 585          } catch (moodle_exception $e) {
 586              $this->assertEquals('invaliduser', $e->errorcode);
 587          }
 588      }
 589  
 590      /*
 591       * Test get scorms by courses
 592       */
 593      public function test_mod_scorm_get_scorms_by_courses() {
 594          global $DB;
 595  
 596          $this->resetAfterTest(true);
 597  
 598          // Create users.
 599          $student = self::getDataGenerator()->create_user();
 600          $teacher = self::getDataGenerator()->create_user();
 601  
 602          // Set to the student user.
 603          self::setUser($student);
 604  
 605          // Create courses to add the modules.
 606          $course1 = self::getDataGenerator()->create_course();
 607          $course2 = self::getDataGenerator()->create_course();
 608  
 609          // First scorm.
 610          $record = new stdClass();
 611          $record->introformat = FORMAT_HTML;
 612          $record->course = $course1->id;
 613          $record->hidetoc = 2;
 614          $record->displayattemptstatus = 2;
 615          $record->skipview = 2;
 616          $scorm1 = self::getDataGenerator()->create_module('scorm', $record);
 617  
 618          // Second scorm.
 619          $record = new stdClass();
 620          $record->introformat = FORMAT_HTML;
 621          $record->course = $course2->id;
 622          $scorm2 = self::getDataGenerator()->create_module('scorm', $record);
 623  
 624          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 625          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
 626  
 627          // Users enrolments.
 628          $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual');
 629          $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, $teacherrole->id, 'manual');
 630  
 631          // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
 632          $enrol = enrol_get_plugin('manual');
 633          $enrolinstances = enrol_get_instances($course2->id, true);
 634          foreach ($enrolinstances as $courseenrolinstance) {
 635              if ($courseenrolinstance->enrol == "manual") {
 636                  $instance2 = $courseenrolinstance;
 637                  break;
 638              }
 639          }
 640          $enrol->enrol_user($instance2, $student->id, $studentrole->id);
 641  
 642          $returndescription = mod_scorm_external::get_scorms_by_courses_returns();
 643  
 644          // Test open/close dates.
 645  
 646          $timenow = time();
 647          $scorm1->timeopen = $timenow - DAYSECS;
 648          $scorm1->timeclose = $timenow - HOURSECS;
 649          $DB->update_record('scorm', $scorm1);
 650  
 651          $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
 652          $result = external_api::clean_returnvalue($returndescription, $result);
 653          $this->assertCount(1, $result['warnings']);
 654          // Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles'.
 655          $this->assertCount(7, $result['scorms'][0]);
 656          $this->assertEquals('expired', $result['warnings'][0]['warningcode']);
 657  
 658          $scorm1->timeopen = $timenow + DAYSECS;
 659          $scorm1->timeclose = $scorm1->timeopen + DAYSECS;
 660          $DB->update_record('scorm', $scorm1);
 661  
 662          $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
 663          $result = external_api::clean_returnvalue($returndescription, $result);
 664          $this->assertCount(1, $result['warnings']);
 665          // Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles'.
 666          $this->assertCount(7, $result['scorms'][0]);
 667          $this->assertEquals('notopenyet', $result['warnings'][0]['warningcode']);
 668  
 669          // Reset times.
 670          $scorm1->timeopen = 0;
 671          $scorm1->timeclose = 0;
 672          $DB->update_record('scorm', $scorm1);
 673  
 674          // Create what we expect to be returned when querying the two courses.
 675          // First for the student user.
 676          $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'version', 'maxgrade',
 677                                  'grademethod', 'whatgrade', 'maxattempt', 'forcecompleted', 'forcenewattempt', 'lastattemptlock',
 678                                  'displayattemptstatus', 'displaycoursestructure', 'sha1hash', 'md5hash', 'revision', 'launch',
 679                                  'skipview', 'hidebrowse', 'hidetoc', 'nav', 'navpositionleft', 'navpositiontop', 'auto',
 680                                  'popup', 'width', 'height', 'timeopen', 'timeclose', 'displayactivityname', 'packagesize',
 681                                  'packageurl', 'scormtype', 'reference');
 682  
 683          // Add expected coursemodule and data.
 684          $scorm1->coursemodule = $scorm1->cmid;
 685          $scorm1->section = 0;
 686          $scorm1->visible = true;
 687          $scorm1->groupmode = 0;
 688          $scorm1->groupingid = 0;
 689  
 690          $scorm2->coursemodule = $scorm2->cmid;
 691          $scorm2->section = 0;
 692          $scorm2->visible = true;
 693          $scorm2->groupmode = 0;
 694          $scorm2->groupingid = 0;
 695  
 696          // SCORM size. The same package is used in both SCORMs.
 697          $scormcontext1 = context_module::instance($scorm1->cmid);
 698          $scormcontext2 = context_module::instance($scorm2->cmid);
 699          $fs = get_file_storage();
 700          $packagefile = $fs->get_file($scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference);
 701          $packagesize = $packagefile->get_filesize();
 702  
 703          $packageurl1 = moodle_url::make_webservice_pluginfile_url(
 704                              $scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference)->out(false);
 705          $packageurl2 = moodle_url::make_webservice_pluginfile_url(
 706                              $scormcontext2->id, 'mod_scorm', 'package', 0, '/', $scorm2->reference)->out(false);
 707  
 708          $scorm1->packagesize = $packagesize;
 709          $scorm1->packageurl = $packageurl1;
 710          $scorm2->packagesize = $packagesize;
 711          $scorm2->packageurl = $packageurl2;
 712  
 713          // Forced to boolean as it is returned as PARAM_BOOL.
 714          $protectpackages = (bool)get_config('scorm', 'protectpackagedownloads');
 715          $expected1 = array('protectpackagedownloads' => $protectpackages);
 716          $expected2 = array('protectpackagedownloads' => $protectpackages);
 717          foreach ($expectedfields as $field) {
 718  
 719              // Since we return the fields used as boolean as PARAM_BOOL instead PARAM_INT we need to force casting here.
 720              // From the returned fields definition we obtain the type expected for the field.
 721              if (empty($returndescription->keys['scorms']->content->keys[$field]->type)) {
 722                  continue;
 723              }
 724              $fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
 725              if ($fieldtype == PARAM_BOOL) {
 726                  $expected1[$field] = (bool) $scorm1->{$field};
 727                  $expected2[$field] = (bool) $scorm2->{$field};
 728              } else {
 729                  $expected1[$field] = $scorm1->{$field};
 730                  $expected2[$field] = $scorm2->{$field};
 731              }
 732          }
 733          $expected1['introfiles'] = [];
 734          $expected2['introfiles'] = [];
 735  
 736          $expectedscorms = array();
 737          $expectedscorms[] = $expected2;
 738          $expectedscorms[] = $expected1;
 739  
 740          // Call the external function passing course ids.
 741          $result = mod_scorm_external::get_scorms_by_courses(array($course2->id, $course1->id));
 742          $result = external_api::clean_returnvalue($returndescription, $result);
 743          $this->assertEquals($expectedscorms, $result['scorms']);
 744  
 745          // Call the external function without passing course id.
 746          $result = mod_scorm_external::get_scorms_by_courses();
 747          $result = external_api::clean_returnvalue($returndescription, $result);
 748          $this->assertEquals($expectedscorms, $result['scorms']);
 749  
 750          // Unenrol user from second course and alter expected scorms.
 751          $enrol->unenrol_user($instance2, $student->id);
 752          array_shift($expectedscorms);
 753  
 754          // Call the external function without passing course id.
 755          $result = mod_scorm_external::get_scorms_by_courses();
 756          $result = external_api::clean_returnvalue($returndescription, $result);
 757          $this->assertEquals($expectedscorms, $result['scorms']);
 758  
 759          // Call for the second course we unenrolled the user from, expected warning.
 760          $result = mod_scorm_external::get_scorms_by_courses(array($course2->id));
 761          $this->assertCount(1, $result['warnings']);
 762          $this->assertEquals('1', $result['warnings'][0]['warningcode']);
 763          $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
 764  
 765          // Now, try as a teacher for getting all the additional fields.
 766          self::setUser($teacher);
 767  
 768          $additionalfields = array('updatefreq', 'timemodified', 'options',
 769                                      'completionstatusrequired', 'completionscorerequired', 'completionstatusallscos',
 770                                      'autocommit', 'section', 'visible', 'groupmode', 'groupingid');
 771  
 772          foreach ($additionalfields as $field) {
 773              $fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
 774  
 775              if ($fieldtype == PARAM_BOOL) {
 776                  $expectedscorms[0][$field] = (bool) $scorm1->{$field};
 777              } else {
 778                  $expectedscorms[0][$field] = $scorm1->{$field};
 779              }
 780          }
 781  
 782          $result = mod_scorm_external::get_scorms_by_courses();
 783          $result = external_api::clean_returnvalue($returndescription, $result);
 784          $this->assertEquals($expectedscorms, $result['scorms']);
 785  
 786          // Even with the SCORM closed in time teacher should retrieve the info.
 787          $scorm1->timeopen = $timenow - DAYSECS;
 788          $scorm1->timeclose = $timenow - HOURSECS;
 789          $DB->update_record('scorm', $scorm1);
 790  
 791          $expectedscorms[0]['timeopen'] = $scorm1->timeopen;
 792          $expectedscorms[0]['timeclose'] = $scorm1->timeclose;
 793  
 794          $result = mod_scorm_external::get_scorms_by_courses();
 795          $result = external_api::clean_returnvalue($returndescription, $result);
 796          $this->assertEquals($expectedscorms, $result['scorms']);
 797  
 798          // Admin also should get all the information.
 799          self::setAdminUser();
 800  
 801          $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
 802          $result = external_api::clean_returnvalue($returndescription, $result);
 803          $this->assertEquals($expectedscorms, $result['scorms']);
 804      }
 805  
 806      /**
 807       * Test launch_sco
 808       */
 809      public function test_launch_sco() {
 810          global $DB;
 811  
 812          // Test invalid instance id.
 813          try {
 814              mod_scorm_external::launch_sco(0);
 815              $this->fail('Exception expected due to invalid mod_scorm instance id.');
 816          } catch (moodle_exception $e) {
 817              $this->assertEquals('invalidrecord', $e->errorcode);
 818          }
 819  
 820          // Test not-enrolled user.
 821          $user = self::getDataGenerator()->create_user();
 822          $this->setUser($user);
 823          try {
 824              mod_scorm_external::launch_sco($this->scorm->id);
 825              $this->fail('Exception expected due to not enrolled user.');
 826          } catch (moodle_exception $e) {
 827              $this->assertEquals('requireloginerror', $e->errorcode);
 828          }
 829  
 830          // Test user with full capabilities.
 831          $this->setUser($this->student);
 832  
 833          // Trigger and capture the event.
 834          $sink = $this->redirectEvents();
 835  
 836          $scoes = scorm_get_scoes($this->scorm->id);
 837          foreach ($scoes as $sco) {
 838              // Find launchable SCO.
 839              if ($sco->launch != '') {
 840                  break;
 841              }
 842          }
 843  
 844          $result = mod_scorm_external::launch_sco($this->scorm->id, $sco->id);
 845          $result = external_api::clean_returnvalue(mod_scorm_external::launch_sco_returns(), $result);
 846  
 847          $events = $sink->get_events();
 848          $this->assertCount(3, $events);
 849          $event = array_pop($events);
 850  
 851          // Checking that the event contains the expected values.
 852          $this->assertInstanceOf('\mod_scorm\event\sco_launched', $event);
 853          $this->assertEquals($this->context, $event->get_context());
 854          $moodleurl = new \moodle_url('/mod/scorm/player.php', array('cm' => $this->cm->id, 'scoid' => $sco->id));
 855          $this->assertEquals($moodleurl, $event->get_url());
 856          $this->assertEventContextNotUsed($event);
 857          $this->assertNotEmpty($event->get_name());
 858  
 859          $event = array_shift($events);
 860          $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
 861  
 862          // Check completion status.
 863          $completion = new completion_info($this->course);
 864          $completiondata = $completion->get_data($this->cm);
 865          $this->assertEquals(COMPLETION_VIEWED, $completiondata->completionstate);
 866  
 867          // Invalid SCO.
 868          try {
 869              mod_scorm_external::launch_sco($this->scorm->id, -1);
 870              $this->fail('Exception expected due to invalid SCO id.');
 871          } catch (moodle_exception $e) {
 872              $this->assertEquals('cannotfindsco', $e->errorcode);
 873          }
 874      }
 875  
 876      /**
 877       * Test mod_scorm_get_scorm_access_information.
 878       */
 879      public function test_mod_scorm_get_scorm_access_information() {
 880          global $DB;
 881  
 882          $this->resetAfterTest(true);
 883  
 884          $student = self::getDataGenerator()->create_user();
 885          $course = self::getDataGenerator()->create_course();
 886          // Create the scorm.
 887          $record = new stdClass();
 888          $record->course = $course->id;
 889          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 890          $context = context_module::instance($scorm->cmid);
 891  
 892          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 893          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
 894  
 895          self::setUser($student);
 896          $result = mod_scorm_external::get_scorm_access_information($scorm->id);
 897          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_access_information_returns(), $result);
 898  
 899          // Check default values for capabilities.
 900          $enabledcaps = array('canskipview', 'cansavetrack', 'canviewscores');
 901  
 902          unset($result['warnings']);
 903          foreach ($result as $capname => $capvalue) {
 904              if (in_array($capname, $enabledcaps)) {
 905                  $this->assertTrue($capvalue);
 906              } else {
 907                  $this->assertFalse($capvalue);
 908              }
 909          }
 910          // Now, unassign one capability.
 911          unassign_capability('mod/scorm:viewscores', $studentrole->id);
 912          array_pop($enabledcaps);
 913          accesslib_clear_all_caches_for_unit_testing();
 914  
 915          $result = mod_scorm_external::get_scorm_access_information($scorm->id);
 916          $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_access_information_returns(), $result);
 917          unset($result['warnings']);
 918          foreach ($result as $capname => $capvalue) {
 919              if (in_array($capname, $enabledcaps)) {
 920                  $this->assertTrue($capvalue);
 921              } else {
 922                  $this->assertFalse($capvalue);
 923              }
 924          }
 925      }
 926  }