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