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.

Differences Between: [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core_xapi;
  18  
  19  use core_xapi\local\statement\item_agent;
  20  use core_xapi\local\statement\item_activity;
  21  use advanced_testcase;
  22  
  23  /**
  24   * Contains test cases for testing xAPI state store methods.
  25   *
  26   * @package    core_xapi
  27   * @since      Moodle 4.2
  28   * @covers     \core_xapi\state_store
  29   * @copyright  2023 Sara Arjona (sara@moodle.com)
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class state_store_test extends advanced_testcase {
  33  
  34      /**
  35       * Setup to ensure that fixtures are loaded.
  36       */
  37      public static function setUpBeforeClass(): void {
  38          global $CFG;
  39          require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
  40      }
  41  
  42      /**
  43       * Testing delete method.
  44       *
  45       * @dataProvider states_provider
  46       * @param array $info Array of overriden state data.
  47       * @param bool $expected Expected results.
  48       * @return void
  49       */
  50      public function test_state_store_delete(array $info, bool $expected): void {
  51          global $DB;
  52  
  53          $this->resetAfterTest();
  54  
  55          // Scenario.
  56          $this->setAdminUser();
  57          // Add, at least, one xAPI state record to database (with the default values).
  58          test_helper::create_state([], true);
  59  
  60          // Get current states in database.
  61          $currentstates = $DB->count_records('xapi_states');
  62  
  63          // Perform test.
  64          $component = $info['component'] ?? 'fake_component';
  65          $state = test_helper::create_state($info);
  66          $store = new state_store($component);
  67          $result = $store->delete($state);
  68  
  69          // Check the state has been removed.
  70          $records = $DB->get_records('xapi_states');
  71          $this->assertTrue($result);
  72          if ($expected) {
  73              $this->assertCount($currentstates - 1, $records);
  74          } else if ($expected === 'false') {
  75              $this->assertCount($currentstates, $records);
  76          }
  77      }
  78  
  79      /**
  80       * Testing get method.
  81       *
  82       * @dataProvider states_provider
  83       * @param array $info Array of overriden state data.
  84       * @param bool $expected Expected results.
  85       * @return void
  86       */
  87      public function test_state_store_get(array $info, bool $expected): void {
  88          $this->resetAfterTest();
  89  
  90          // Scenario.
  91          $this->setAdminUser();
  92          // Add, at least, one xAPI state record to database (with the default values).
  93          test_helper::create_state([], true);
  94  
  95          // Perform test.
  96          $component = $info['component'] ?? 'fake_component';
  97          $state = test_helper::create_state($info);
  98          // Remove statedata from the state object, to guarantee the get method is working as expected.
  99          $state->set_state_data(null);
 100          $store = new state_store($component);
 101          $result = $store->get($state);
 102  
 103          // Check the returned state has the expected values.
 104          if ($expected) {
 105              $this->assertEquals(json_encode($state->jsonSerialize()), json_encode($result->jsonSerialize()));
 106          } else {
 107              $this->assertNull($result);
 108          }
 109      }
 110  
 111      /**
 112       * Data provider for the test_state_store_delete and test_state_store_get tests.
 113       *
 114       * @return array
 115       */
 116      public function states_provider() : array {
 117          return [
 118              'Existing and valid state' => [
 119                  'info' => [],
 120                  'expected' => true,
 121              ],
 122              'No state (wrong activityid)' => [
 123                  'info' => ['activity' => item_activity::create_from_id('1')],
 124                  'expected' => false,
 125              ],
 126              'No state (wrong stateid)' => [
 127                  'info' => ['stateid' => 'food'],
 128                  'expected' => false,
 129              ],
 130              'No state (wrong component)' => [
 131                  'info' => ['component' => 'mod_h5pactivity'],
 132                  'expected' => false,
 133              ],
 134          ];
 135      }
 136  
 137      /**
 138       * Testing put method.
 139       *
 140       * @dataProvider put_states_provider
 141       * @param array $info Array of overriden state data.
 142       * @param string $expected Expected results.
 143       * @return void
 144       */
 145      public function test_state_store_put(array $info, string $expected): void {
 146          global $DB;
 147  
 148          $this->resetAfterTest();
 149  
 150          // Scenario.
 151          $this->setAdminUser();
 152          // Add, at least, one xAPI state record to database (with the default values).
 153          test_helper::create_state([], true);
 154  
 155          // Get current states in database.
 156          $currentstates = $DB->count_records('xapi_states');
 157  
 158          // Perform test.
 159          $component = $info['component'] ?? 'fake_component';
 160          $state = test_helper::create_state($info);
 161          $store = new state_store($component);
 162          $result = $store->put($state);
 163  
 164          // Check the state has been added/updated.
 165          $this->assertTrue($result);
 166          $recordsnum = $DB->count_records('xapi_states');
 167          $params = [
 168              'component' => $component,
 169              'userid' => $state->get_user()->id,
 170              'itemid' => $state->get_activity_id(),
 171              'stateid' => $state->get_state_id(),
 172              'registration' => $state->get_registration(),
 173          ];
 174          $records = $DB->get_records('xapi_states', $params);
 175          $record = reset($records);
 176          if ($expected === 'added') {
 177              $this->assertEquals($currentstates + 1, $recordsnum);
 178              $this->assertEquals($record->timecreated, $record->timemodified);
 179          } else if ($expected === 'updated') {
 180              $this->assertEquals($currentstates, $recordsnum);
 181              $this->assertGreaterThanOrEqual($record->timecreated, $record->timemodified);
 182          }
 183  
 184          $this->assertEquals($component, $record->component);
 185          $this->assertEquals($state->get_activity_id(), $record->itemid);
 186          $this->assertEquals($state->get_user()->id, $record->userid);
 187          $this->assertEquals(json_encode($state->jsonSerialize()), $record->statedata);
 188          $this->assertEquals($state->get_registration(), $record->registration);
 189      }
 190  
 191      /**
 192       * Data provider for the test_state_store_put tests.
 193       *
 194       * @return array
 195       */
 196      public function put_states_provider() : array {
 197          return [
 198              'Update existing state' => [
 199                  'info' => [],
 200                  'expected' => 'updated',
 201              ],
 202              'Update existing state (change statedata)' => [
 203                  'info' => ['statedata' => '{"progress":0,"answers":[[["BB"],[""]],[{"answers":[]}]],"answered":[true,false]}'],
 204                  'expected' => 'updated',
 205              ],
 206              'Add state (with different itemid)' => [
 207                  'info' => ['activity' => item_activity::create_from_id('1')],
 208                  'expected' => 'added',
 209              ],
 210              'Add state (with different stateid)' => [
 211                  'info' => ['stateid' => 'food'],
 212                  'expected' => 'added',
 213              ],
 214              'Add state (with different component)' => [
 215                  'info' => ['component' => 'mod_h5pactivity'],
 216                  'expected' => 'added',
 217              ],
 218          ];
 219      }
 220  
 221      /**
 222       * Testing reset method.
 223       *
 224       * @dataProvider reset_wipe_states_provider
 225       * @param array $info Array of overriden state data.
 226       * @param int $expected The states that will be reset.
 227       * @return void
 228       */
 229      public function test_state_store_reset(array $info, int $expected): void {
 230          global $DB;
 231  
 232          $this->resetAfterTest();
 233  
 234          // Scenario.
 235          $this->setAdminUser();
 236          $other = $this->getDataGenerator()->create_user();
 237  
 238          // Add a few xAPI state records to database.
 239          test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
 240          test_helper::create_state(['activity' => item_activity::create_from_id('2'), 'stateid' => 'paella'], true);
 241          test_helper::create_state([
 242              'activity' => item_activity::create_from_id('3'),
 243              'agent' => item_agent::create_from_user($other),
 244              'stateid' => 'paella',
 245              'registration' => 'ABC',
 246          ], true);
 247          test_helper::create_state([
 248              'activity' => item_activity::create_from_id('4'),
 249              'agent' => item_agent::create_from_user($other),
 250          ], true);
 251          test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
 252          test_helper::create_state([
 253              'activity' => item_activity::create_from_id('6'),
 254              'component' => 'my_component',
 255              'stateid' => 'paella',
 256              'agent' => item_agent::create_from_user($other),
 257          ], true);
 258  
 259          // Get current states in database.
 260          $currentstates = $DB->count_records('xapi_states');
 261  
 262          // Perform test.
 263          $component = $info['component'] ?? 'fake_component';
 264          $itemid = $info['activity'] ?? null;
 265          $userid = (array_key_exists('agent', $info) && $info['agent'] === 'other') ? $other->id : null;
 266          $stateid = $info['stateid'] ?? null;
 267          $registration = $info['registration'] ?? null;
 268          $store = new state_store($component);
 269          $store->reset($itemid, $userid, $stateid, $registration);
 270  
 271          // Check the states haven't been removed.
 272          $this->assertCount($currentstates, $DB->get_records('xapi_states'));
 273          $records = $DB->get_records_select('xapi_states', 'statedata IS NULL');
 274          $this->assertCount($expected, $records);
 275      }
 276  
 277      /**
 278       * Testing wipe method.
 279       *
 280       * @dataProvider reset_wipe_states_provider
 281       * @param array $info Array of overriden state data.
 282       * @param int $expected The removed states.
 283       * @return void
 284       */
 285      public function test_state_store_wipe(array $info, int $expected): void {
 286          global $DB;
 287  
 288          $this->resetAfterTest();
 289  
 290          // Scenario.
 291          $this->setAdminUser();
 292          $other = $this->getDataGenerator()->create_user();
 293  
 294          // Add a few xAPI state records to database.
 295          test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
 296          test_helper::create_state(['activity' => item_activity::create_from_id('2'), 'stateid' => 'paella'], true);
 297          test_helper::create_state([
 298              'activity' => item_activity::create_from_id('3'),
 299              'agent' => item_agent::create_from_user($other),
 300              'stateid' => 'paella',
 301              'registration' => 'ABC',
 302          ], true);
 303          test_helper::create_state([
 304              'activity' => item_activity::create_from_id('4'),
 305              'agent' => item_agent::create_from_user($other),
 306          ], true);
 307          test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
 308          test_helper::create_state([
 309              'activity' => item_activity::create_from_id('6'),
 310              'component' => 'my_component',
 311              'stateid' => 'paella',
 312              'agent' => item_agent::create_from_user($other),
 313          ], true);
 314  
 315          // Get current states in database.
 316          $currentstates = $DB->count_records('xapi_states');
 317  
 318          // Perform test.
 319          $component = $info['component'] ?? 'fake_component';
 320          $itemid = $info['activity'] ?? null;
 321          $userid = (array_key_exists('agent', $info) && $info['agent'] === 'other') ? $other->id : null;
 322          $stateid = $info['stateid'] ?? null;
 323          $registration = $info['registration'] ?? null;
 324          $store = new state_store($component);
 325          $store->wipe($itemid, $userid, $stateid, $registration);
 326  
 327          // Check the states have been removed.
 328          $records = $DB->get_records('xapi_states');
 329          $this->assertCount($currentstates - $expected, $records);
 330      }
 331  
 332      /**
 333       * Data provider for the test_state_store_reset and test_state_store_wipe tests.
 334       *
 335       * @return array
 336       */
 337      public function reset_wipe_states_provider() : array {
 338          return [
 339              'With fake_component' => [
 340                  'info' => [],
 341                  'expected' => 4,
 342              ],
 343              'With my_component' => [
 344                  'info' => ['component' => 'my_component'],
 345                  'expected' => 2,
 346              ],
 347              'With unexisting_component' => [
 348                  'info' => ['component' => 'unexisting_component'],
 349                  'expected' => 0,
 350              ],
 351              'Existing activity' => [
 352                  'info' => ['activity' => '1'],
 353                  'expected' => 1,
 354              ],
 355              'Unexisting activity' => [
 356                  'info' => ['activity' => '1111'],
 357                  'expected' => 0,
 358              ],
 359              'Existing userid' => [
 360                  'info' => ['agent' => 'other'],
 361                  'expected' => 2,
 362              ],
 363              'Existing stateid' => [
 364                  'info' => ['stateid' => 'paella'],
 365                  'expected' => 2,
 366              ],
 367              'Unexisting stateid' => [
 368                  'info' => ['stateid' => 'chorizo'],
 369                  'expected' => 0,
 370              ],
 371              'Existing registration' => [
 372                  'info' => ['registration' => 'ABC'],
 373                  'expected' => 1,
 374              ],
 375              'Uxexisting registration' => [
 376                  'info' => ['registration' => 'XYZ'],
 377                  'expected' => 0,
 378              ],
 379              'Existing stateid combined with activity' => [
 380                  'info' => ['activity' => '3', 'stateid' => 'paella'],
 381                  'expected' => 1,
 382              ],
 383              'Uxexisting stateid combined with activity' => [
 384                  'info' => ['activity' => '1', 'stateid' => 'paella'],
 385                  'expected' => 0,
 386              ],
 387          ];
 388      }
 389  
 390      /**
 391       * Testing cleanup method.
 392       *
 393       * @return void
 394       */
 395      public function test_state_store_cleanup(): void {
 396          global $DB;
 397  
 398          $this->resetAfterTest();
 399  
 400          // Scenario.
 401          $this->setAdminUser();
 402          $other = $this->getDataGenerator()->create_user();
 403  
 404          // Add a few xAPI state records to database.
 405          test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
 406          test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
 407          test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
 408          test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
 409          test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
 410          test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'my_component'], true);
 411  
 412          // Get current states in database.
 413          $currentstates = $DB->count_records('xapi_states');
 414  
 415          // Perform test.
 416          $component = 'fake_component';
 417          $store = new state_store($component);
 418          $store->cleanup();
 419  
 420          // Check no state has been removed (because the entries are not old enough).
 421          $this->assertEquals($currentstates, $DB->count_records('xapi_states'));
 422  
 423          // Make the existing state entries older.
 424          $timepast = time() - 2;
 425          $DB->set_field('xapi_states', 'timecreated', $timepast);
 426          $DB->set_field('xapi_states', 'timemodified', $timepast);
 427  
 428          // Create 1 more state, that shouldn't be removed after the cleanup.
 429          test_helper::create_state(['activity' => item_activity::create_from_id('7')], true);
 430  
 431          // Set the config to remove states older than 1 second.
 432          set_config('xapicleanupperiod', 1);
 433  
 434          // Check old states for fake_component have been removed.
 435          $currentstates = $DB->count_records('xapi_states');
 436          $store->cleanup();
 437          $this->assertEquals($currentstates - 4, $DB->count_records('xapi_states'));
 438          $this->assertEquals(1, $DB->count_records('xapi_states', ['component' => $component]));
 439          $this->assertEquals(2, $DB->count_records('xapi_states', ['component' => 'my_component']));
 440      }
 441  
 442      /**
 443       * Testing get_state_ids method.
 444       *
 445       * @dataProvider get_state_ids_provider
 446       * @param string $component
 447       * @param string|null $itemid
 448       * @param string|null $registration
 449       * @param bool|null $since
 450       * @param array $expected the expected result
 451       * @return void
 452       */
 453      public function test_get_state_ids(
 454          string $component,
 455          ?string $itemid,
 456          ?string $registration,
 457          ?bool $since,
 458          array $expected,
 459      ): void {
 460          global $DB, $USER;
 461  
 462          $this->resetAfterTest();
 463  
 464          // Scenario.
 465          $this->setAdminUser();
 466          $other = $this->getDataGenerator()->create_user();
 467  
 468          // Add a few xAPI state records to database.
 469          $states = [
 470              ['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'],
 471              ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'bb'],
 472              ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg2', 'stateid' => 'cc'],
 473              ['activity' => item_activity::create_from_id('2'), 'registration' => 'reg', 'stateid' => 'dd'],
 474              ['activity' => item_activity::create_from_id('3'), 'stateid' => 'ee'],
 475              ['activity' => item_activity::create_from_id('4'), 'component' => 'other', 'stateid' => 'ff'],
 476          ];
 477          foreach ($states as $state) {
 478              test_helper::create_state($state, true);
 479          }
 480  
 481          // Make all existing state entries older except form two.
 482          $currenttime = time();
 483          $timepast = $currenttime - 5;
 484          $DB->set_field('xapi_states', 'timecreated', $timepast);
 485          $DB->set_field('xapi_states', 'timemodified', $timepast);
 486          $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'aa']);
 487          $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'bb']);
 488          $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'dd']);
 489  
 490          // Perform test.
 491          $sincetime = ($since) ? $currenttime - 1 : null;
 492          $store = new state_store($component);
 493          $stateids = $store->get_state_ids($itemid, $USER->id, $registration, $sincetime);
 494          sort($stateids);
 495  
 496          $this->assertEquals($expected, $stateids);
 497      }
 498  
 499      /**
 500       * Data provider for the test_get_state_ids.
 501       *
 502       * @return array
 503       */
 504      public function get_state_ids_provider(): array {
 505          return [
 506              'empty_component' => [
 507                  'component' => 'empty_component',
 508                  'itemid' => null,
 509                  'registration' => null,
 510                  'since' => null,
 511                  'expected' => [],
 512              ],
 513              'filter_by_itemid' => [
 514                  'component' => 'fake_component',
 515                  'itemid' => '1',
 516                  'registration' => null,
 517                  'since' => null,
 518                  'expected' => ['aa', 'bb', 'cc'],
 519              ],
 520              'filter_by_registration' => [
 521                  'component' => 'fake_component',
 522                  'itemid' => null,
 523                  'registration' => 'reg',
 524                  'since' => null,
 525                  'expected' => ['bb', 'dd'],
 526              ],
 527              'filter_by_since' => [
 528                  'component' => 'fake_component',
 529                  'itemid' => null,
 530                  'registration' => null,
 531                  'since' => true,
 532                  'expected' => ['aa', 'bb', 'dd'],
 533              ],
 534              'filter_by_itemid_and_registration' => [
 535                  'component' => 'fake_component',
 536                  'itemid' => '1',
 537                  'registration' => 'reg',
 538                  'since' => null,
 539                  'expected' => ['bb'],
 540              ],
 541              'filter_by_itemid_registration_since' => [
 542                  'component' => 'fake_component',
 543                  'itemid' => '1',
 544                  'registration' => 'reg',
 545                  'since' => true,
 546                  'expected' => ['bb'],
 547              ],
 548              'filter_by_registration_since' => [
 549                  'component' => 'fake_component',
 550                  'itemid' => null,
 551                  'registration' => 'reg',
 552                  'since' => true,
 553                  'expected' => ['bb', 'dd'],
 554              ],
 555          ];
 556      }
 557  
 558      /**
 559       * Test delete with a non numeric activity id.
 560       *
 561       * The default state store only allows integer itemids.
 562       *
 563       * @dataProvider invalid_activityid_format_provider
 564       * @param string $operation the method to execute
 565       * @param bool $usestate if the param is a state or the activity id
 566       */
 567      public function test_invalid_activityid_format(string $operation, bool $usestate = false): void {
 568          $this->resetAfterTest();
 569          $this->setAdminUser();
 570  
 571          $state = test_helper::create_state([
 572              'activity' => item_activity::create_from_id('notnumeric'),
 573          ]);
 574          $param = ($usestate) ? $state : 'notnumeric';
 575  
 576          $this->expectException(xapi_exception::class);
 577          $store = new state_store('fake_component');
 578          $store->$operation($param);
 579      }
 580  
 581      /**
 582       * Data provider for test_invalid_activityid_format.
 583       *
 584       * @return array
 585       */
 586      public function invalid_activityid_format_provider(): array {
 587          return [
 588              'delete' => [
 589                  'operation' => 'delete',
 590                  'usestate' => true,
 591              ],
 592              'get' => [
 593                  'operation' => 'get',
 594                  'usestate' => true,
 595              ],
 596              'put' => [
 597                  'operation' => 'put',
 598                  'usestate' => true,
 599              ],
 600              'reset' => [
 601                  'operation' => 'reset',
 602                  'usestate' => false,
 603              ],
 604              'wipe' => [
 605                  'operation' => 'wipe',
 606                  'usestate' => false,
 607              ],
 608              'get_state_ids' => [
 609                  'operation' => 'get_state_ids',
 610                  'usestate' => false,
 611              ],
 612          ];
 613      }
 614  }