Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  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(7, $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(7, $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', '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', 'displayactivityname', '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  
 685          $scorm2->coursemodule = $scorm2->cmid;
 686          $scorm2->section = 0;
 687          $scorm2->visible = true;
 688          $scorm2->groupmode = 0;
 689          $scorm2->groupingid = 0;
 690  
 691          // SCORM size. The same package is used in both SCORMs.
 692          $scormcontext1 = \context_module::instance($scorm1->cmid);
 693          $scormcontext2 = \context_module::instance($scorm2->cmid);
 694          $fs = get_file_storage();
 695          $packagefile = $fs->get_file($scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference);
 696          $packagesize = $packagefile->get_filesize();
 697  
 698          $packageurl1 = \moodle_url::make_webservice_pluginfile_url(
 699                              $scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference)->out(false);
 700          $packageurl2 = \moodle_url::make_webservice_pluginfile_url(
 701                              $scormcontext2->id, 'mod_scorm', 'package', 0, '/', $scorm2->reference)->out(false);
 702  
 703          $scorm1->packagesize = $packagesize;
 704          $scorm1->packageurl = $packageurl1;
 705          $scorm2->packagesize = $packagesize;
 706          $scorm2->packageurl = $packageurl2;
 707  
 708          // Forced to boolean as it is returned as PARAM_BOOL.
 709          $protectpackages = (bool)get_config('scorm', 'protectpackagedownloads');
 710          $expected1 = array('protectpackagedownloads' => $protectpackages);
 711          $expected2 = array('protectpackagedownloads' => $protectpackages);
 712          foreach ($expectedfields as $field) {
 713  
 714              // Since we return the fields used as boolean as PARAM_BOOL instead PARAM_INT we need to force casting here.
 715              // From the returned fields definition we obtain the type expected for the field.
 716              if (empty($returndescription->keys['scorms']->content->keys[$field]->type)) {
 717                  continue;
 718              }
 719              $fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
 720              if ($fieldtype == PARAM_BOOL) {
 721                  $expected1[$field] = (bool) $scorm1->{$field};
 722                  $expected2[$field] = (bool) $scorm2->{$field};
 723              } else {
 724                  $expected1[$field] = $scorm1->{$field};
 725                  $expected2[$field] = $scorm2->{$field};
 726              }
 727          }
 728          $expected1['introfiles'] = [];
 729          $expected2['introfiles'] = [];
 730  
 731          $expectedscorms = array();
 732          $expectedscorms[] = $expected2;
 733          $expectedscorms[] = $expected1;
 734  
 735          // Call the external function passing course ids.
 736          $result = mod_scorm_external::get_scorms_by_courses(array($course2->id, $course1->id));
 737          $result = \external_api::clean_returnvalue($returndescription, $result);
 738          $this->assertEquals($expectedscorms, $result['scorms']);
 739  
 740          // Call the external function without passing course id.
 741          $result = mod_scorm_external::get_scorms_by_courses();
 742          $result = \external_api::clean_returnvalue($returndescription, $result);
 743          $this->assertEquals($expectedscorms, $result['scorms']);
 744  
 745          // Unenrol user from second course and alter expected scorms.
 746          $enrol->unenrol_user($instance2, $student->id);
 747          array_shift($expectedscorms);
 748  
 749          // Call the external function without passing course id.
 750          $result = mod_scorm_external::get_scorms_by_courses();
 751          $result = \external_api::clean_returnvalue($returndescription, $result);
 752          $this->assertEquals($expectedscorms, $result['scorms']);
 753  
 754          // Call for the second course we unenrolled the user from, expected warning.
 755          $result = mod_scorm_external::get_scorms_by_courses(array($course2->id));
 756          $this->assertCount(1, $result['warnings']);
 757          $this->assertEquals('1', $result['warnings'][0]['warningcode']);
 758          $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
 759  
 760          // Now, try as a teacher for getting all the additional fields.
 761          self::setUser($teacher);
 762  
 763          $additionalfields = array('updatefreq', 'timemodified', 'options',
 764                                      'completionstatusrequired', 'completionscorerequired', 'completionstatusallscos',
 765                                      'autocommit', 'section', 'visible', 'groupmode', 'groupingid');
 766  
 767          foreach ($additionalfields as $field) {
 768              $fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
 769  
 770              if ($fieldtype == PARAM_BOOL) {
 771                  $expectedscorms[0][$field] = (bool) $scorm1->{$field};
 772              } else {
 773                  $expectedscorms[0][$field] = $scorm1->{$field};
 774              }
 775          }
 776  
 777          $result = mod_scorm_external::get_scorms_by_courses();
 778          $result = \external_api::clean_returnvalue($returndescription, $result);
 779          $this->assertEquals($expectedscorms, $result['scorms']);
 780  
 781          // Even with the SCORM closed in time teacher should retrieve the info.
 782          $scorm1->timeopen = $timenow - DAYSECS;
 783          $scorm1->timeclose = $timenow - HOURSECS;
 784          $DB->update_record('scorm', $scorm1);
 785  
 786          $expectedscorms[0]['timeopen'] = $scorm1->timeopen;
 787          $expectedscorms[0]['timeclose'] = $scorm1->timeclose;
 788  
 789          $result = mod_scorm_external::get_scorms_by_courses();
 790          $result = \external_api::clean_returnvalue($returndescription, $result);
 791          $this->assertEquals($expectedscorms, $result['scorms']);
 792  
 793          // Admin also should get all the information.
 794          self::setAdminUser();
 795  
 796          $result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
 797          $result = \external_api::clean_returnvalue($returndescription, $result);
 798          $this->assertEquals($expectedscorms, $result['scorms']);
 799      }
 800  
 801      /**
 802       * Test launch_sco
 803       */
 804      public function test_launch_sco() {
 805          global $DB;
 806  
 807          // Test invalid instance id.
 808          try {
 809              mod_scorm_external::launch_sco(0);
 810              $this->fail('Exception expected due to invalid mod_scorm instance id.');
 811          } catch (\moodle_exception $e) {
 812              $this->assertEquals('invalidrecord', $e->errorcode);
 813          }
 814  
 815          // Test not-enrolled user.
 816          $user = self::getDataGenerator()->create_user();
 817          $this->setUser($user);
 818          try {
 819              mod_scorm_external::launch_sco($this->scorm->id);
 820              $this->fail('Exception expected due to not enrolled user.');
 821          } catch (\moodle_exception $e) {
 822              $this->assertEquals('requireloginerror', $e->errorcode);
 823          }
 824  
 825          // Test user with full capabilities.
 826          $this->setUser($this->student);
 827  
 828          // Trigger and capture the event.
 829          $sink = $this->redirectEvents();
 830  
 831          $scoes = scorm_get_scoes($this->scorm->id);
 832          foreach ($scoes as $sco) {
 833              // Find launchable SCO.
 834              if ($sco->launch != '') {
 835                  break;
 836              }
 837          }
 838  
 839          $result = mod_scorm_external::launch_sco($this->scorm->id, $sco->id);
 840          $result = \external_api::clean_returnvalue(mod_scorm_external::launch_sco_returns(), $result);
 841  
 842          $events = $sink->get_events();
 843          $this->assertCount(3, $events);
 844          $event = array_pop($events);
 845  
 846          // Checking that the event contains the expected values.
 847          $this->assertInstanceOf('\mod_scorm\event\sco_launched', $event);
 848          $this->assertEquals($this->context, $event->get_context());
 849          $moodleurl = new \moodle_url('/mod/scorm/player.php', array('cm' => $this->cm->id, 'scoid' => $sco->id));
 850          $this->assertEquals($moodleurl, $event->get_url());
 851          $this->assertEventContextNotUsed($event);
 852          $this->assertNotEmpty($event->get_name());
 853  
 854          $event = array_shift($events);
 855          $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
 856  
 857          // Check completion status.
 858          $completion = new \completion_info($this->course);
 859          $completiondata = $completion->get_data($this->cm);
 860          $this->assertEquals(COMPLETION_VIEWED, $completiondata->completionstate);
 861  
 862          // Invalid SCO.
 863          try {
 864              mod_scorm_external::launch_sco($this->scorm->id, -1);
 865              $this->fail('Exception expected due to invalid SCO id.');
 866          } catch (\moodle_exception $e) {
 867              $this->assertEquals('cannotfindsco', $e->errorcode);
 868          }
 869      }
 870  
 871      /**
 872       * Test mod_scorm_get_scorm_access_information.
 873       */
 874      public function test_mod_scorm_get_scorm_access_information() {
 875          global $DB;
 876  
 877          $this->resetAfterTest(true);
 878  
 879          $student = self::getDataGenerator()->create_user();
 880          $course = self::getDataGenerator()->create_course();
 881          // Create the scorm.
 882          $record = new \stdClass();
 883          $record->course = $course->id;
 884          $scorm = self::getDataGenerator()->create_module('scorm', $record);
 885          $context = \context_module::instance($scorm->cmid);
 886  
 887          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 888          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
 889  
 890          self::setUser($student);
 891          $result = mod_scorm_external::get_scorm_access_information($scorm->id);
 892          $result = \external_api::clean_returnvalue(mod_scorm_external::get_scorm_access_information_returns(), $result);
 893  
 894          // Check default values for capabilities.
 895          $enabledcaps = array('canskipview', 'cansavetrack', 'canviewscores');
 896  
 897          unset($result['warnings']);
 898          foreach ($result as $capname => $capvalue) {
 899              if (in_array($capname, $enabledcaps)) {
 900                  $this->assertTrue($capvalue);
 901              } else {
 902                  $this->assertFalse($capvalue);
 903              }
 904          }
 905          // Now, unassign one capability.
 906          unassign_capability('mod/scorm:viewscores', $studentrole->id);
 907          array_pop($enabledcaps);
 908          accesslib_clear_all_caches_for_unit_testing();
 909  
 910          $result = mod_scorm_external::get_scorm_access_information($scorm->id);
 911          $result = \external_api::clean_returnvalue(mod_scorm_external::get_scorm_access_information_returns(), $result);
 912          unset($result['warnings']);
 913          foreach ($result as $capname => $capvalue) {
 914              if (in_array($capname, $enabledcaps)) {
 915                  $this->assertTrue($capvalue);
 916              } else {
 917                  $this->assertFalse($capvalue);
 918              }
 919          }
 920      }
 921  }