Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 and 311] [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  }