Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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  declare(strict_types = 1);
  18  
  19  namespace mod_scorm;
  20  
  21  use advanced_testcase;
  22  use cm_info;
  23  use coding_exception;
  24  use mod_scorm\completion\custom_completion;
  25  use moodle_exception;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  global $CFG;
  30  require_once($CFG->libdir . '/completionlib.php');
  31  require_once($CFG->dirroot.'/mod/scorm/locallib.php');
  32  
  33  /**
  34   * Class for unit testing mod_scorm/custom_completion.
  35   *
  36   * @package   mod_scorm
  37   * @copyright 2021 Michael Hawkins <michaelh@moodle.com>
  38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class custom_completion_test extends advanced_testcase {
  41  
  42      /**
  43       * Data provider for get_state().
  44       *
  45       * @return array[]
  46       */
  47      public function get_state_provider(): array {
  48  
  49          // Prepare various reusable user scorm track data used to mock various completion states/requirements.
  50          $completionincomplete = (object) [
  51              'id' => 1,
  52              'scoid' => 1,
  53              'element' => 'cmi.completion_status',
  54              'value' => 'incomplete',
  55          ];
  56  
  57          $completionpassed = (object) [
  58              'id' => 1,
  59              'scoid' => 1,
  60              'element' => 'cmi.completion_status',
  61              'value' => 'passed',
  62          ];
  63  
  64          $completioncompleted = (object) [
  65              'id' => 1,
  66              'scoid' => 2,
  67              'element' => 'cmi.success_status',
  68              'value' => 'completed',
  69          ];
  70  
  71          $completionscorefail = (object) [
  72              'id' => 1,
  73              'scoid' => 1,
  74              'element' => 'cmi.score.raw',
  75              'value' => '20',
  76          ];
  77  
  78          $completionscorepass = (object) [
  79              'id' => 1,
  80              'scoid' => 1,
  81              'element' => 'cmi.score.raw',
  82              'value' => '100',
  83          ];
  84  
  85          return [
  86              'Undefined completion requirement' => [
  87                  'somenonexistentrule', COMPLETION_ENABLED, [$completionincomplete], 0, null, coding_exception::class
  88              ],
  89              'Completion status requirement not available' => [
  90                  'completionstatusrequired', COMPLETION_DISABLED, [$completionincomplete], 0, null, moodle_exception::class
  91              ],
  92              'Completion status Passed required, user has no completion status recorded' => [
  93                  'completionstatusrequired', 2, [], 0, COMPLETION_INCOMPLETE, null
  94              ],
  95              'Completion status Passed required, user has not passed, can make another attempt' => [
  96                  'completionstatusrequired', 2, [$completionincomplete], 0, COMPLETION_INCOMPLETE, null
  97              ],
  98              'Completion status Passed required, user has passed' => [
  99                  'completionstatusrequired', 2, [$completionpassed], 0, COMPLETION_COMPLETE, null
 100              ],
 101              'Completion status Completed required, user has not completed, can make another attempt' => [
 102                  'completionstatusrequired', 4, [$completionincomplete], 2, COMPLETION_INCOMPLETE, null
 103              ],
 104              'Completion status Completed required, user has completed' => [
 105                  'completionstatusrequired', 4, [$completioncompleted], 1, COMPLETION_COMPLETE, null
 106              ],
 107              'Completion status Passed and Completed required, user has only completed, can make another attempt' => [
 108                  'completionstatusrequired', 6, [$completioncompleted], 0, COMPLETION_COMPLETE, null
 109              ],
 110              'Completion status Passed and Completed required, user has completed and passed' => [
 111                  'completionstatusrequired', 6, [$completionpassed, $completioncompleted], 0, COMPLETION_COMPLETE, null
 112              ],
 113              'Completion status Passed and Completed required, user has not passed or completed, but has another attempt' => [
 114                  'completionstatusrequired', 6, [$completionincomplete], 2, COMPLETION_INCOMPLETE, null
 115              ],
 116              'Completion status Passed and Completed required, user has used all attempts, but not passed or completed' => [
 117                  'completionstatusrequired', 6, [$completionincomplete], 1, COMPLETION_COMPLETE_FAIL, null
 118              ],
 119              'Completion status Passed required, user has used all attempts and completed, but not passed' => [
 120                  'completionstatusrequired', 2, [$completioncompleted], 1, COMPLETION_COMPLETE_FAIL, null
 121              ],
 122              'Completion status Completed required, user has used all attempts, but not completed' => [
 123                  'completionstatusrequired', 4, [$completionincomplete], 1, COMPLETION_COMPLETE_FAIL, null
 124              ],
 125              'Completion status Passed and Completed required, user has used all attempts, but not passed' => [
 126                  'completionstatusrequired', 6, [$completionincomplete, $completioncompleted], 2, COMPLETION_COMPLETE, null
 127              ],
 128              'Completion score required, user has no score' => [
 129                  'completionscorerequired', 80, [], 0, COMPLETION_INCOMPLETE, null
 130              ],
 131              'Completion score required, user score does not meet requirement, can make another attempt' => [
 132                  'completionscorerequired', 80, [$completionscorefail], 0, COMPLETION_INCOMPLETE, null
 133              ],
 134              'Completion score required, user has used all attempts, but not reached the score' => [
 135                  'completionscorerequired', 80, [$completionscorefail], 1, COMPLETION_COMPLETE_FAIL, null
 136              ],
 137              'Completion score required, user score meets requirement' => [
 138                  'completionscorerequired', 80, [$completionscorepass], 0, COMPLETION_COMPLETE, null
 139              ],
 140              'Completion of all scos required, user has not completed, can make another attempt' => [
 141                  'completionstatusallscos', 1, [$completionincomplete, $completioncompleted], 3, COMPLETION_INCOMPLETE, null
 142              ],
 143              'Completion of all scos required, user has completed' => [
 144                  'completionstatusallscos', 1, [$completionpassed, $completioncompleted], 2, COMPLETION_COMPLETE, null
 145              ],
 146              'Completion of all scos required, user has used all attempts, but not completed all scos' => [
 147                  'completionstatusallscos', 1, [$completionincomplete, $completioncompleted], 2, COMPLETION_COMPLETE_FAIL, null
 148              ],
 149          ];
 150      }
 151  
 152      /**
 153       * Test for get_state().
 154       *
 155       * @dataProvider get_state_provider
 156       * @param string $rule The custom completion condition.
 157       * @param int $rulevalue The custom completion rule value.
 158       * @param array $uservalue The relevant record database mock data recorded against the user for the rule.
 159       * @param int $maxattempts The number of attempts the activity allows (0 = unlimited).
 160       * @param int|null $status Expected completion status for the rule.
 161       * @param string|null $exception Expected exception.
 162       */
 163      public function test_get_state(string $rule, int $rulevalue, array $uservalue, int $maxattempts, ?int $status,
 164              ?string $exception) {
 165          global $DB;
 166  
 167          if (!is_null($exception)) {
 168              $this->expectException($exception);
 169          }
 170  
 171          // Custom completion rule data for cm_info::customdata.
 172          $customdataval = [
 173              'customcompletionrules' => [
 174                  $rule => $rulevalue
 175              ]
 176          ];
 177  
 178          // Build a mock cm_info instance.
 179          $mockcminfo = $this->getMockBuilder(cm_info::class)
 180              ->disableOriginalConstructor()
 181              ->onlyMethods(['__get'])
 182              ->getMock();
 183  
 184          // Mock the return of the magic getter method when fetching the cm_info object's
 185          // customdata and instance values.
 186          $mockcminfo->expects($this->any())
 187              ->method('__get')
 188              ->will($this->returnValueMap([
 189                  ['customdata', $customdataval],
 190                  ['instance', 1],
 191              ]));
 192  
 193          // Mock the DB call fetching user's SCORM track data.
 194          $DB = $this->createMock(get_class($DB));
 195          $DB->expects($this->atMost(1))
 196              ->method('get_records_sql')
 197              ->willReturn($uservalue);
 198  
 199          // For completed all scos tests, mock the DB call that fetches the sco IDs.
 200          if ($rule === 'completionstatusallscos') {
 201              $returnscos = [];
 202  
 203              foreach ($uservalue as $data) {
 204                  $returnscos[$data->scoid] = (object) ['id' => $data->scoid];
 205              }
 206  
 207              $DB->expects($this->atMost(1))
 208                  ->method('get_records')
 209                  ->willReturn($returnscos);
 210          }
 211  
 212          // Anything not complete will check if attempts have been exhausted, mock the DB calls for that check.
 213          if ($status != COMPLETION_COMPLETE) {
 214              $mockscorm = (object) [
 215                  'id' => 1,
 216                  'version' => SCORM_13,
 217                  'grademethod' => GRADESCOES,
 218                  'maxattempt' => $maxattempts,
 219              ];
 220  
 221              $DB->expects($this->atMost(1))
 222                  ->method('get_record')
 223                  ->willReturn($mockscorm);
 224  
 225              $DB->expects($this->atMost(1))
 226                  ->method('count_records_sql')
 227                  ->willReturn(count($uservalue));
 228          }
 229  
 230          $customcompletion = new custom_completion($mockcminfo, 2);
 231  
 232          $this->assertEquals($status, $customcompletion->get_state($rule));
 233      }
 234  
 235      /**
 236       * Test for get_defined_custom_rules().
 237       */
 238      public function test_get_defined_custom_rules() {
 239          $expectedrules = [
 240              'completionstatusrequired',
 241              'completionscorerequired',
 242              'completionstatusallscos',
 243          ];
 244  
 245          $definedrules = custom_completion::get_defined_custom_rules();
 246          $this->assertCount(3, $definedrules);
 247  
 248          foreach ($definedrules as $definedrule) {
 249              $this->assertContains($definedrule, $expectedrules);
 250          }
 251      }
 252  
 253      /**
 254       * Test for get_defined_custom_rule_descriptions().
 255       */
 256      public function test_get_custom_rule_descriptions() {
 257          // Get defined custom rules.
 258          $rules = custom_completion::get_defined_custom_rules();
 259  
 260          // Build a mock cm_info instance.
 261          $mockcminfo = $this->getMockBuilder(cm_info::class)
 262              ->disableOriginalConstructor()
 263              ->onlyMethods(['__get'])
 264              ->getMock();
 265  
 266          // Instantiate a custom_completion object using the mocked cm_info.
 267          $customcompletion = new custom_completion($mockcminfo, 1);
 268  
 269          // Get custom rule descriptions.
 270          $ruledescriptions = $customcompletion->get_custom_rule_descriptions();
 271  
 272          // Confirm that defined rules and rule descriptions are consistent with each other.
 273          $this->assertEquals(count($rules), count($ruledescriptions));
 274          foreach ($rules as $rule) {
 275              $this->assertArrayHasKey($rule, $ruledescriptions);
 276          }
 277      }
 278  
 279      /**
 280       * Test for is_defined().
 281       */
 282      public function test_is_defined() {
 283          // Build a mock cm_info instance.
 284          $mockcminfo = $this->getMockBuilder(cm_info::class)
 285              ->disableOriginalConstructor()
 286              ->getMock();
 287  
 288          $customcompletion = new custom_completion($mockcminfo, 1);
 289  
 290          // All rules are defined.
 291          $this->assertTrue($customcompletion->is_defined('completionstatusrequired'));
 292          $this->assertTrue($customcompletion->is_defined('completionscorerequired'));
 293          $this->assertTrue($customcompletion->is_defined('completionstatusallscos'));
 294  
 295          // Undefined rule is not found.
 296          $this->assertFalse($customcompletion->is_defined('somerandomrule'));
 297      }
 298  
 299      /**
 300       * Data provider for test_get_available_custom_rules().
 301       *
 302       * @return array[]
 303       */
 304      public function get_available_custom_rules_provider(): array {
 305          return [
 306              'Completion status enabled only' => [
 307                  [
 308                      'completionstatusrequired' => 4,
 309                      'completionscorerequired' => COMPLETION_DISABLED,
 310                      'completionstatusallscos' => COMPLETION_DISABLED,
 311                  ],
 312                  ['completionstatusrequired'],
 313              ],
 314              'Completion score enabled only' => [
 315                  [
 316                      'completionstatusrequired' => COMPLETION_DISABLED,
 317                      'completionscorerequired' => 80,
 318                      'completionstatusallscos' => COMPLETION_DISABLED,
 319                  ],
 320                  ['completionscorerequired'],
 321              ],
 322              'Completion status and all scos completed both enabled' => [
 323                  [
 324                      'completionstatusrequired' => 2,
 325                      'completionscorerequired' => COMPLETION_DISABLED,
 326                      'completionstatusallscos' => COMPLETION_ENABLED,
 327                  ],
 328                  ['completionstatusrequired', 'completionstatusallscos'],
 329              ],
 330              'Completion status and score both enabled' => [
 331                  [
 332                      'completionstatusrequired' => COMPLETION_ENABLED,
 333                      'completionscorerequired' => 80,
 334                      'completionstatusallscos' => COMPLETION_DISABLED,
 335                  ],
 336                  ['completionstatusrequired', 'completionscorerequired'],
 337              ],
 338              'All custom completion conditions enabled' => [
 339                  [
 340                      'completionstatusrequired' => 6,
 341                      'completionscorerequired' => 80,
 342                      'completionstatusallscos' => COMPLETION_ENABLED,
 343                  ],
 344                  ['completionstatusrequired', 'completionscorerequired', 'completionstatusallscos'],
 345              ],
 346          ];
 347      }
 348  
 349      /**
 350       * Test for get_available_custom_rules().
 351       *
 352       * @dataProvider get_available_custom_rules_provider
 353       * @param array $completionrulesvalues
 354       * @param array $expected
 355       */
 356      public function test_get_available_custom_rules(array $completionrulesvalues, array $expected) {
 357          $customcompletionrules = [
 358              'customcompletionrules' => $completionrulesvalues,
 359          ];
 360  
 361          // Build a mock cm_info instance.
 362          $mockcminfo = $this->getMockBuilder(cm_info::class)
 363              ->disableOriginalConstructor()
 364              ->onlyMethods(['__get'])
 365              ->getMock();
 366  
 367          // Mock the return of magic getter for the customdata attribute.
 368          $mockcminfo->expects($this->any())
 369              ->method('__get')
 370              ->with('customdata')
 371              ->willReturn($customcompletionrules);
 372  
 373          $customcompletion = new custom_completion($mockcminfo, 1);
 374          $this->assertEquals($expected, $customcompletion->get_available_custom_rules());
 375      }
 376  }