Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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