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.
   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_xapi\xapi_exception;
  20  use core_xapi\local\statement\item_agent;
  21  use externallib_advanced_testcase;
  22  use core_external\external_api;
  23  use core_xapi\iri;
  24  use core_xapi\local\state;
  25  use core_xapi\local\statement\item_activity;
  26  use core_xapi\test_helper;
  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 get states webservice.
  35   *
  36   * @package    core_xapi
  37   * @covers     \core_xapi\external\get_states
  38   * @since      Moodle 4.2
  39   * @copyright  2023 Ferran Recio <ferran@moodle.com>
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class get_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       * Execute the get_states service from a generate state.
  54       *
  55       * @param string $component component name
  56       * @param state $data the original state to extract the params
  57       * @param string|null $since the formated timestamp or ISO 8601 date
  58       * @param array $override overridden params
  59       * @return string[] array of state ids
  60       */
  61      private function execute_service(
  62          string $component,
  63          state $data,
  64          ?string $since = null,
  65          array $override = []
  66      ): array {
  67          // Apply overrides.
  68          $activityiri = $override['activityiri'] ?? iri::generate($data->get_activity_id(), 'activity');
  69          $registration = $override['registration'] ?? $data->get_registration();
  70          $agent = $override['agent'] ?? $data->get_agent();
  71          if (!empty($override['user'])) {
  72              $agent = item_agent::create_from_user($override['user']);
  73          }
  74  
  75          $external = $this->get_external_class();
  76          $result = $external::execute(
  77              $component,
  78              $activityiri,
  79              json_encode($agent),
  80              $registration,
  81              $since
  82          );
  83          $result = external_api::clean_returnvalue($external::execute_returns(), $result);
  84  
  85          // Sorting result to make them comparable.
  86          sort($result);
  87          return $result;
  88      }
  89  
  90      /**
  91       * Return a xAPI external webservice class to operate.
  92       *
  93       * The test needs to fake a component in order to test without
  94       * using a real one. This way if in the future any component
  95       * implement it's xAPI handler this test will continue working.
  96       *
  97       * @return get_states the external class
  98       */
  99      private function get_external_class(): get_states {
 100          $ws = new class extends get_states {
 101              /**
 102               * Method to override validate_component.
 103               *
 104               * @param string $component  The component name in frankenstyle.
 105               */
 106              protected static function validate_component(string $component): void {
 107                  if ($component != 'fake_component') {
 108                      parent::validate_component($component);
 109                  }
 110              }
 111          };
 112          return $ws;
 113      }
 114  
 115      /**
 116       * Testing different component names on valid states.
 117       *
 118       * @dataProvider components_provider
 119       * @param string $component component name
 120       * @param string|null $exception expect exception
 121       */
 122      public function test_component_names(string $component, ?bool $exception): void {
 123          $this->resetAfterTest();
 124  
 125          // Scenario.
 126          $this->setAdminUser();
 127  
 128          // Add, at least, one xAPI state record to database.
 129          $data = test_helper::create_state(
 130              ['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'],
 131              true
 132          );
 133  
 134          // If no result is expected we will just incur in exception.
 135          if ($exception) {
 136              $this->expectException(xapi_exception::class);
 137          }
 138  
 139          $result = $this->execute_service($component, $data);
 140          $this->assertEquals(['aa'], $result);
 141      }
 142  
 143      /**
 144       * Data provider for the test_component_names tests.
 145       *
 146       * @return  array
 147       */
 148      public function components_provider() : array {
 149          return [
 150              'Inexistent component' => [
 151                  'component' => 'inexistent_component',
 152                  'exception' => true,
 153              ],
 154              'Compatible component' => [
 155                  'component' => 'fake_component',
 156                  'exception' => false,
 157              ],
 158              'Incompatible component' => [
 159                  'component' => 'core_xapi',
 160                  'exception' => true,
 161              ],
 162          ];
 163      }
 164  
 165      /**
 166       * Testing different since date formats.
 167       *
 168       * @dataProvider since_formats_provider
 169       * @param string|null $since the formatted timestamps
 170       * @param string[]|null $expected expected results
 171       * @param bool $exception expect exception
 172       */
 173      public function test_since_formats(?string $since, ?array $expected, bool $exception = false): void {
 174          $this->resetAfterTest();
 175          $this->setAdminUser();
 176  
 177          $states = $this->generate_states();
 178  
 179          if ($exception) {
 180              $this->expectException(xapi_exception::class);
 181          }
 182  
 183          $result = $this->execute_service('fake_component', $states['aa'], $since);
 184          $this->assertEquals($expected, $result);
 185      }
 186  
 187      /**
 188       * Data provider for the test_since_formats tests.
 189       *
 190       * @return  array
 191       */
 192      public function since_formats_provider(): array {
 193          return [
 194              'Null date' => [
 195                  'since' => null,
 196                  'expected' => ['aa', 'bb', 'cc', 'dd'],
 197                  'exception' => false,
 198              ],
 199              'Numeric timestamp' => [
 200                  'since' => '1651100399',
 201                  'expected' => ['aa', 'bb'],
 202                  'exception' => false,
 203              ],
 204              'ISO 8601 format 1' => [
 205                  'since' => '2022-04-28T06:59',
 206                  'expected' => ['aa', 'bb'],
 207                  'exception' => false,
 208              ],
 209              'ISO 8601 format 2' => [
 210                  'since' => '2022-04-28T06:59:59',
 211                  'expected' => ['aa', 'bb'],
 212                  'exception' => false,
 213              ],
 214              'Wrong format' => [
 215                  'since' => 'Spanish omelette without onion',
 216                  'expected' => null,
 217                  'exception' => true,
 218              ],
 219          ];
 220      }
 221  
 222      /**
 223       * Testing different activity IRI values.
 224       *
 225       * @dataProvider activity_iri_provider
 226       * @param string|null $activityiri
 227       * @param string[]|null $expected expected results
 228       */
 229      public function test_activity_iri(?string $activityiri, ?array $expected): void {
 230          $this->resetAfterTest();
 231          $this->setAdminUser();
 232  
 233          $states = $this->generate_states();
 234  
 235          $override = ['activityiri' => $activityiri];
 236          $result = $this->execute_service('fake_component', $states['aa'], null, $override);
 237          $this->assertEquals($expected, $result);
 238      }
 239  
 240      /**
 241       * Data provider for the test_activity_iri tests.
 242       *
 243       * @return  array
 244       */
 245      public function activity_iri_provider(): array {
 246          return [
 247              'Activity with several states' => [
 248                  'activityiri' => iri::generate('1', 'activity'),
 249                  'expected' => ['aa', 'bb', 'cc', 'dd'],
 250              ],
 251              'Activity with one state' => [
 252                  'activityiri' => iri::generate('2', 'activity'),
 253                  'expected' => ['ee'],
 254              ],
 255              'Inexistent activity' => [
 256                  'activityiri' => iri::generate('3', 'activity'),
 257                  'expected' => [],
 258              ],
 259          ];
 260      }
 261  
 262      /**
 263       * Testing different agent values.
 264       *
 265       * @dataProvider agent_values_provider
 266       * @param string|null $agentreference the used agent reference
 267       * @param string[]|null $expected expected results
 268       * @param bool $exception expect exception
 269       */
 270      public function test_agent_values(?string $agentreference, ?array $expected, bool $exception = false): void {
 271          $this->resetAfterTest();
 272          $this->setAdminUser();
 273  
 274          $states = $this->generate_states();
 275  
 276          if ($exception) {
 277              $this->expectException(xapi_exception::class);
 278          }
 279  
 280          $userreferences = [
 281              'current' => $states['aa']->get_user(),
 282              'other' => $this->getDataGenerator()->create_user(),
 283          ];
 284  
 285          $override = [
 286              'user' => $userreferences[$agentreference],
 287          ];
 288          $result = $this->execute_service('fake_component', $states['aa'], null, $override);
 289          $this->assertEquals($expected, $result);
 290      }
 291  
 292      /**
 293       * Data provider for the test_agent_values tests.
 294       *
 295       * @return  array
 296       */
 297      public function agent_values_provider(): array {
 298          return [
 299              'Current user' => [
 300                  'agentreference' => 'current',
 301                  'expected' => ['aa', 'bb', 'cc', 'dd'],
 302                  'exception' => false,
 303              ],
 304              'Other user' => [
 305                  'agentreference' => 'other',
 306                  'expected' => null,
 307                  'exception' => true,
 308              ],
 309          ];
 310      }
 311  
 312      /**
 313       * Testing different registration values.
 314       *
 315       * @dataProvider registration_values_provider
 316       * @param string|null $registration
 317       * @param string[]|null $expected expected results
 318       */
 319      public function test_registration_values(?string $registration, ?array $expected): void {
 320          $this->resetAfterTest();
 321          $this->setAdminUser();
 322  
 323          $states = $this->generate_states();
 324  
 325          $override = ['registration' => $registration];
 326          $result = $this->execute_service('fake_component', $states['aa'], null, $override);
 327          $this->assertEquals($expected, $result);
 328      }
 329  
 330      /**
 331       * Data provider for the test_registration_values tests.
 332       *
 333       * @return  array
 334       */
 335      public function registration_values_provider(): array {
 336          return [
 337              'Null registration' => [
 338                  'registration' => null,
 339                  'expected' => ['aa', 'bb', 'cc', 'dd'],
 340              ],
 341              'Registration with one state id' => [
 342                  'registration' => 'reg2',
 343                  'expected' => ['cc'],
 344              ],
 345              'Registration with two state ids' => [
 346                  'registration' => 'reg',
 347                  'expected' => ['bb', 'dd'],
 348              ],
 349              'Registration with no state ids' => [
 350                  'registration' => 'invented',
 351                  'expected' => [],
 352              ],
 353          ];
 354      }
 355  
 356      /**
 357       * Generate the state for the testing scenarios.
 358       *
 359       * Generate a variaty of states from several components, registrations and state ids.
 360       * Some of the states are registered as they are done in 27-04-2022 07:00:00 while others
 361       * are updated in 28-04-2022 07:00:00.
 362       *
 363       * @return state[]
 364       */
 365      private function generate_states(): array {
 366          global $DB;
 367  
 368          $testdate = \DateTime::createFromFormat('d-m-Y H:i:s', '28-04-2022 07:00:00');
 369          // Unix timestamp: 1651100400.
 370          $currenttime = $testdate->getTimestamp();
 371  
 372          $result = [];
 373  
 374          // Add a few xAPI state records to database.
 375          $states = [
 376              ['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'],
 377              ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'bb'],
 378              ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg2', 'stateid' => 'cc'],
 379              ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'dd'],
 380              ['activity' => item_activity::create_from_id('2'), 'stateid' => 'ee'],
 381              ['activity' => item_activity::create_from_id('3'), 'component' => 'other', 'stateid' => 'gg'],
 382              ['activity' => item_activity::create_from_id('3'), 'component' => 'other', 'registration' => 'reg', 'stateid' => 'ff'],
 383          ];
 384          foreach ($states as $state) {
 385              $result[$state['stateid']] = test_helper::create_state($state, true);
 386          }
 387  
 388          $timepast = $currenttime - DAYSECS;
 389          $DB->set_field('xapi_states', 'timecreated', $timepast);
 390          $DB->set_field('xapi_states', 'timemodified', $timepast);
 391          $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'aa']);
 392          $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'bb']);
 393          $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'ee']);
 394  
 395          return $result;
 396      }
 397  }