Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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 core_xapi\external;
  18  
  19  use core\context\module;
  20  use core_external\external_api;
  21  use core_xapi\iri;
  22  use core_xapi\local\statement\item_activity;
  23  use core_xapi\local\statement\item_agent;
  24  use core_xapi\test_helper;
  25  use core_xapi\xapi_exception;
  26  use externallib_advanced_testcase;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  global $CFG;
  31  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  32  
  33  /**
  34   * Unit tests for xAPI delete states webservice.
  35   *
  36   * @package    core_xapi
  37   * @covers     \core_xapi\external\delete_states
  38   * @since      Moodle 4.3
  39   * @copyright  2023 Laurent David <laurent.david@moodle.com>
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class delete_states_test extends externallib_advanced_testcase {
  43  
  44      /**
  45       * Setup to ensure that fixtures are loaded.
  46       */
  47      public static function setUpBeforeClass(): void {
  48          global $CFG;
  49          require_once($CFG->dirroot . '/lib/xapi/tests/helper.php');
  50      }
  51  
  52      /**
  53       * Testing different component names on valid states.
  54       *
  55       * @dataProvider components_provider
  56       * @param string $component component name
  57       * @param object|null $expected expected results
  58       */
  59      public function test_component_names(string $component, ?object $expected): void {
  60          global $DB, $USER;
  61          $this->resetAfterTest();
  62  
  63          // Scenario.
  64          $this->setAdminUser();
  65  
  66          // Perform test.
  67          $info = [
  68              'agent' => item_agent::create_from_user($USER),
  69              'activity' => item_activity::create_from_id('12345'),
  70          ];
  71          test_helper::create_state($info, true);
  72          if (!empty($expected->exception)) {
  73              $this->expectException($expected->exception);
  74          }
  75          $this->execute($component,
  76              iri::generate($info['activity']->get_id(), 'activity'),
  77              json_encode($info['agent'])
  78          );
  79  
  80          if (isset($expected->expectedcount)) {
  81              $this->assertEquals($expected->expectedcount, $DB->record_exists('xapi_states', []));
  82          }
  83      }
  84  
  85      /**
  86       * This function execute the delete_states_data
  87       *
  88       * @param string $component component name
  89       * @param string $activityiri
  90       * @param string $agent
  91       * @param string|null $registration
  92       * @return array empty array
  93       */
  94      private function execute(string $component,
  95          string $activityiri,
  96          string $agent,
  97          ?string $registration = null
  98      ): void {
  99          $external = $this->get_external_class();
 100          $external::execute(
 101              $component,
 102              $activityiri,
 103              $agent,
 104              $registration
 105          );
 106      }
 107  
 108      /**
 109       * Return a xAPI external webservice class to operate.
 110       *
 111       * The test needs to fake a component in order to test without
 112       * using a real one. This way if in the future any component
 113       * implement it's xAPI handler this test will continue working.
 114       *
 115       * @return delete_states the external class
 116       */
 117      private function get_external_class(): delete_states {
 118          $ws = new class extends delete_states {
 119              /**
 120               * Method to override validate_component.
 121               *
 122               * @param string $component The component name in frankenstyle.
 123               */
 124              protected static function validate_component(string $component): void {
 125                  if ($component != 'fake_component') {
 126                      parent::validate_component($component);
 127                  }
 128              }
 129          };
 130          return $ws;
 131      }
 132  
 133      /**
 134       * Data provider for the test_component_names tests.
 135       *
 136       * @return  array
 137       */
 138      public function components_provider(): array {
 139          return [
 140              'Inexistent component' => [
 141                  'component' => 'inexistent_component',
 142                  'expected' => (object) ['exception' => xapi_exception::class],
 143              ],
 144              'Compatible component' => [
 145                  'component' => 'fake_component',
 146                  'expected' => (object) ['expectedcount' => 0],
 147              ],
 148              'Incompatible component' => [
 149                  'component' => 'core_xapi',
 150                  'expected' => (object) ['exception' => xapi_exception::class],
 151              ],
 152          ];
 153      }
 154  
 155      /**
 156       * Testing invalid agent.
 157       *
 158       */
 159      public function test_invalid_agent(): void {
 160          $this->resetAfterTest();
 161  
 162          // Scenario.
 163          $this->setAdminUser();
 164          $other = $this->getDataGenerator()->create_user();
 165  
 166          // Invalid agent (use different user, instead of the current one).
 167          $info = [
 168              'agent' => item_agent::create_from_user($other),
 169              'activity' => item_activity::create_from_id('12345'),
 170          ];
 171          test_helper::create_state($info, true);
 172          $this->expectException(xapi_exception::class);
 173          $this->execute(
 174              'fake_component',
 175              iri::generate($info['activity']->get_id(), 'activity'),
 176              json_encode($info['agent'])
 177          );
 178      }
 179  
 180      /**
 181       * Testing deleting states
 182       *
 183       * @dataProvider states_provider
 184       * @param string $testedusername
 185       * @param string $testedcomponent
 186       * @param string $testedactivityname
 187       * @param array $states
 188       * @param array $expectedstates
 189       * @return void
 190       */
 191      public function test_delete_states(string $testedusername,
 192          string $testedcomponent,
 193          string $testedactivityname,
 194          array $states,
 195          array $expectedstates
 196      ): void {
 197          global $DB;
 198          $this->resetAfterTest();
 199  
 200          // Scenario.
 201          $this->setAdminUser();
 202          $course = $this->getDataGenerator()->create_course();
 203  
 204          $activities = [];
 205          $users = [];
 206          // Create a set of states for different users and components.
 207          foreach ($states as $stateinfo) {
 208              $params = [
 209                  'component' => $stateinfo['component'] ?? 'mod_h5pactivity',
 210              ];
 211              $uname = $stateinfo['user'];
 212              $user = $users[$uname] ?? null;
 213              if (empty($user)) {
 214                  $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
 215                  $users[$uname] = $user;
 216              }
 217              $activityname = $stateinfo['activity'];
 218              $activity = $activities[$activityname] ?? null;
 219              if (empty($activity)) {
 220                  if (empty($stateinfo['activityid'])) {
 221                      $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
 222                      $activitycontext = module::instance($activity->cmid);
 223                      $activities[$activityname] = item_activity::create_from_id($activitycontext->id);
 224                  } else {
 225                      $activities[$activityname] = item_activity::create_from_id($stateinfo['activityid']);
 226                  }
 227              }
 228              $params['activity'] = $activities[$activityname];
 229              $params['agent'] = item_agent::create_from_user($user);
 230              test_helper::create_state($params, true);
 231          }
 232          if (empty($users[$testedusername])) {
 233              $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
 234              $users[$testedusername] = $user;
 235          }
 236          $this->setUser($users[$testedusername]);
 237          $activity = $activities[$testedactivityname];
 238          $activityiri = iri::generate($activity->get_id(), 'activity');
 239          $agent = json_encode(item_agent::create_from_user($users[$testedusername]));
 240          $this->execute($testedcomponent,
 241              $activityiri,
 242              $agent);
 243  
 244          $statesleft = $DB->get_records('xapi_states');
 245          // Check that we have the expected leftover records.
 246          $this->assertCount(count($expectedstates), $statesleft);
 247          foreach ($expectedstates as $expectedstate) {
 248              $expectedactivityid = $activities[$expectedstate['activity']]->get_id();
 249              $expecteduserid = $users[$expectedstate['user']]->id;
 250              $found = false;
 251              foreach ($statesleft as $state) {
 252                  if ($state->userid == $expecteduserid && $state->itemid == $expectedactivityid) {
 253                      $found = true;
 254                      break;
 255                  }
 256              }
 257              $this->assertTrue($found, 'State not found:' . json_encode($statesleft));
 258          }
 259      }
 260  
 261      /**
 262       * Data provider for the test_get_state tests.
 263       *
 264       * @return array
 265       */
 266      public function states_provider(): array {
 267          return [
 268              'Activities with different users and components' => [
 269                  'username' => 'user1',
 270                  'component' => 'mod_h5pactivity',
 271                  'activity' => 'Activity 1',
 272                  'states' => [
 273                      [
 274                          'user' => 'user1',
 275                          'activity' => 'Activity 1',
 276                          'component' => 'mod_h5pactivity'
 277                      ],
 278                      [
 279                          'user' => 'user2',
 280                          'activity' => 'Activity 1',
 281                          'component' => 'mod_h5pactivity'
 282                      ],
 283                      [
 284                          'user' => 'user1',
 285                          'activity' => 'Activity 3',
 286                          'activityid' => '1',
 287                          'component' => 'core_xapi'
 288                      ],
 289                      [
 290                          'user' => 'user1',
 291                          'activity' => 'Activity 1',
 292                          'component' => 'mod_h5pactivity'
 293                      ],
 294                  ],
 295                  'expectedstatesleft' => [
 296                      ['user' => 'user2', 'activity' => 'Activity 1'],
 297                      ['user' => 'user1', 'activity' => 'Activity 3']
 298                  ]
 299              ],
 300              'Activities with one single user' => [
 301                  'username' => 'user1',
 302                  'component' => 'mod_h5pactivity',
 303                  'activity' => 'Activity 1',
 304                  'states' => [
 305                      [
 306                          'user' => 'user1',
 307                          'activity' => 'Activity 1',
 308                          'component' => 'mod_h5pactivity'
 309                      ],
 310                      [
 311                          'user' => 'user1',
 312                          'activity' => 'Activity 1',
 313                          'component' => 'mod_h5pactivity'
 314                      ],
 315                      [
 316                          'user' => 'user1',
 317                          'activity' => 'Activity 1',
 318                          'component' => 'mod_h5pactivity'
 319                      ],
 320                  ],
 321                  'expectedstatesleft' => []
 322              ],
 323          ];
 324      }
 325  }