Search moodle.org's
Developer Documentation

See Release Notes

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

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

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