Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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