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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 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  /**
  18   * This file contains unit test related to xAPI library.
  19   *
  20   * @package    core_xapi
  21   * @copyright  2020 Ferran Recio
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core_xapi\external;
  25  
  26  use core_xapi\xapi_exception;
  27  use core_xapi\test_helper;
  28  use core_xapi\external\post_statement;
  29  use core_xapi\local\statement;
  30  use core_xapi\local\statement\item_agent;
  31  use core_xapi\local\statement\item_group;
  32  use core_xapi\local\statement\item_verb;
  33  use core_xapi\local\statement\item_activity;
  34  use externallib_advanced_testcase;
  35  use stdClass;
  36  use core_external\external_api;
  37  
  38  defined('MOODLE_INTERNAL') || die();
  39  
  40  global $CFG;
  41  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  42  
  43  /**
  44   * Unit tests for xAPI statement processing webservice.
  45   *
  46   * @package    core_xapi
  47   * @since      Moodle 3.9
  48   * @copyright  2020 Ferran Recio
  49   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  50   */
  51  class post_statement_test extends externallib_advanced_testcase {
  52  
  53      /** @var test_helper for generating valid xapi statements. */
  54      private $testhelper;
  55  
  56      /**
  57       * Setup to ensure that fixtures are loaded.
  58       */
  59      public static function setupBeforeClass(): void {
  60          global $CFG;
  61          require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
  62      }
  63  
  64      /**
  65       * Setup test.
  66       */
  67      public function setUp(): void {
  68          global $CFG;
  69          // We disable group actors on the test xapi_handler.
  70          $CFG->xapitestforcegroupactors = false;
  71      }
  72  
  73      /**
  74       * Return a xAPI external webservice class to operate.
  75       *
  76       * The test needs to fake a component in order to test without
  77       * using a real one. This way if in the future any component
  78       * implement it's xAPI handler this test will continue working.
  79       *
  80       * @return post_statement the external class
  81       */
  82      private function get_extenal_class(): post_statement {
  83          $ws = new class extends post_statement {
  84              protected static function validate_component(string $component): void {
  85                  if ($component != 'fake_component') {
  86                      parent::validate_component($component);
  87                  }
  88              }
  89          };
  90          return $ws;
  91      }
  92  
  93      /**
  94       * This function do all checks from a standard post_statements request.
  95       *
  96       * The reason for this function is because statements crafting (special in error
  97       * scenarios) is complicated to do via data providers because every test need a specific
  98       * testing conditions. For this reason alls tests creates a scenario and then uses this
  99       * function to check the results.
 100       *
 101       * @param string $component component name
 102       * @param mixed $data data to encode and send to post_statement
 103       * @param array $expected expected results (i empty an exception is expected)
 104       */
 105      private function post_statements_data(string $component, $data, array $expected) {
 106          global $USER;
 107  
 108          $testhelper = new test_helper();
 109          $testhelper->init_log();
 110  
 111          // If no result is expected we will just incur in exception.
 112          if (empty($expected)) {
 113              $this->expectException(xapi_exception::class);
 114          } else {
 115              $this->preventResetByRollback(); // Logging waits till the transaction gets committed.
 116          }
 117  
 118          $json = json_encode($data);
 119  
 120          $external = $this->get_extenal_class();
 121          $result = $external::execute($component, $json);
 122          $result = external_api::clean_returnvalue($external::execute_returns(), $result);
 123  
 124          // Check results.
 125          $this->assertCount(count($expected), $result);
 126          foreach ($expected as $key => $expect) {
 127              $this->assertEquals($expect, $result[$key]);
 128          }
 129  
 130          // Check log entries.
 131          $log = $testhelper->get_last_log_entry();
 132          $this->assertNotEmpty($log);
 133  
 134          // Validate statement information on log.
 135          $value = $log->get_name();
 136          $this->assertEquals($value, 'xAPI test statement');
 137          $value = $log->get_description();
 138          // Due to logstore limitation, event must use a real component (core_xapi).
 139          $this->assertEquals($value, "User '{$USER->id}' send a statement to component 'core_xapi'");
 140      }
 141  
 142      /**
 143       * Return a valid statement object with the params passed.
 144       *
 145       * All tests are based on craft different types os statements. This function
 146       * is made to provent redundant code on the test.
 147       *
 148       * @param array $items array of overriden statement items (default [])
 149       * @return statement the resulting statement
 150       */
 151      private function get_valid_statement(array $items = []): statement {
 152          global $USER;
 153  
 154          $actor = $items['actor'] ?? item_agent::create_from_user($USER);
 155          $verb = $items['verb'] ?? item_verb::create_from_id('cook');
 156          $object = $items['object'] ?? item_activity::create_from_id('paella');
 157  
 158          $statement = new statement();
 159          $statement->set_actor($actor);
 160          $statement->set_verb($verb);
 161          $statement->set_object($object);
 162  
 163          return $statement;
 164      }
 165  
 166      /**
 167       * Testing different component names on valid statements.
 168       *
 169       * @dataProvider components_provider
 170       * @param string $component component name
 171       * @param array $expected expected results
 172       */
 173      public function test_component_names(string $component, array $expected) {
 174  
 175          $this->resetAfterTest();
 176  
 177          // Scenario.
 178          $this->setAdminUser();
 179  
 180          // Perform test.
 181          $data = $this->get_valid_statement();
 182          $this->post_statements_data ($component, $data, $expected);
 183      }
 184  
 185      /**
 186       * Data provider for the test_component_names tests.
 187       *
 188       * @return  array
 189       */
 190      public function components_provider() : array {
 191          return [
 192              'Inexistent component' => [
 193                  'inexistent_component', []
 194              ],
 195              'Compatible component' => [
 196                  'fake_component', [true]
 197              ],
 198              'Incompatible component' => [
 199                  'core_xapi', []
 200              ],
 201          ];
 202      }
 203  
 204      /**
 205       * Testing raw JSON encoding.
 206       *
 207       * This test is used for wrong json format and empty structures.
 208       *
 209       * @dataProvider invalid_json_provider
 210       * @param string $json json string to send
 211       */
 212      public function test_invalid_json(string $json) {
 213  
 214          $this->resetAfterTest();
 215  
 216          // Scenario.
 217          $this->setAdminUser();
 218  
 219          // Perform test.
 220          $testhelper = new test_helper();
 221          $testhelper->init_log();
 222  
 223          // If no result is expected we will just incur in exception.
 224          $this->expectException(xapi_exception::class);
 225  
 226          $external = $this->get_extenal_class();
 227          $result = $external::execute('fake_component', $json);
 228          $result = external_api::clean_returnvalue($external::execute_returns(), $result);
 229      }
 230  
 231      /**
 232       * Data provider for the test_components tests.
 233       *
 234       * @return  array
 235       */
 236      public function invalid_json_provider() : array {
 237          return [
 238              'Wrong json' => [
 239                  'This is not { a json object /'
 240              ],
 241              'Empty string json' => [
 242                  ''
 243              ],
 244              'Empty array json' => [
 245                  '[]'
 246              ],
 247              'Invalid single statement json' => [
 248                  '{"actor":{"objectType":"Agent","mbox":"noemail@moodle.org"},"verb":{"id":"InvalidVerb"}'
 249                  .',"object":{"objectType":"Activity","id":"somethingwrong"}}'
 250              ],
 251              'Invalid multiple statement json' => [
 252                  '[{"actor":{"objectType":"Agent","mbox":"noemail@moodle.org"},"verb":{"id":"InvalidVerb"}'
 253                  .',"object":{"objectType":"Activity","id":"somethingwrong"}}]'
 254              ],
 255          ];
 256      }
 257  
 258      /**
 259       * Testing agent (user) statements.
 260       *
 261       * This function test several scenarios using different combinations
 262       * of statement rejection motives. Some motives produces a full batch
 263       * rejection (exception) and other can leed to indivual rejection on
 264       * each statement. For example,try to post a statement without $USER
 265       * in it produces a full batch rejection, while using an invalid
 266       * verb on one statement just reject that specific statement
 267       * That is the expected behaviour.
 268       *
 269       * @dataProvider statement_provider
 270       * @param bool $multiple if send multiple statements (adds one valid statement)
 271       * @param bool $validactor if the actor used is valid
 272       * @param bool $validverb if the verb used is valid
 273       * @param array $expected expected results
 274       */
 275      public function test_statements_agent(bool $multiple, bool $validactor, bool $validverb, array $expected) {
 276          global $USER;
 277  
 278          $this->resetAfterTest();
 279  
 280          $this->setAdminUser();
 281  
 282          $other = $this->getDataGenerator()->create_user();
 283  
 284          $info = [];
 285  
 286          // Setup actor.
 287          if ($validactor) {
 288              $info['actor'] = item_agent::create_from_user($USER);
 289          } else {
 290              $info['actor'] = item_agent::create_from_user($other);
 291          }
 292  
 293          // Setup verb.
 294          if (!$validverb) {
 295              $info['verb'] = item_verb::create_from_id('invalid');
 296          }
 297  
 298          $data = $this->get_valid_statement($info);
 299  
 300          if ($multiple) {
 301              $data = [
 302                  $this->get_valid_statement(),
 303                  $data,
 304              ];
 305          }
 306  
 307          // Perform test.
 308          $this->post_statements_data ('fake_component', $data, $expected);
 309      }
 310  
 311      /**
 312       * Testing group statements.
 313       *
 314       * This function test several scenarios using different combinations
 315       * of statement rejection motives. Some motives produces a full batch
 316       * rejection (exception) and other can leed to indivual rejection on
 317       * each statement. For example,try to post a statement without $USER
 318       * in it produces a full batch rejection, while using an invalid
 319       * verb on one statement just reject that specific statement
 320       * That is the expected behaviour.
 321       *
 322       * @dataProvider statement_provider
 323       * @param bool $multiple if send multiple statements (adds one valid statement)
 324       * @param bool $validactor if the actor used is valid
 325       * @param bool $validverb if the verb used is valid
 326       * @param array $expected expected results
 327       */
 328      public function test_statements_group(bool $multiple, bool $validactor, bool $validverb, array $expected) {
 329          global $USER, $CFG;
 330  
 331          $this->resetAfterTest();
 332  
 333          $this->setAdminUser();
 334  
 335          $other = $this->getDataGenerator()->create_user();
 336  
 337          $info = [];
 338  
 339          // Enable group mode in the handle.
 340          $CFG->xapitestforcegroupactors = true;
 341  
 342          // Create one course and 1 group.
 343          $course = $this->getDataGenerator()->create_course();
 344          $this->getDataGenerator()->enrol_user($USER->id, $course->id);
 345          $this->getDataGenerator()->enrol_user($other->id, $course->id);
 346  
 347          $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 348          $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $other->id));
 349  
 350          if ($validactor) {
 351              // Add $USER into a group to make group valid for processing.
 352              $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $USER->id));
 353          }
 354          $info['actor'] = item_group::create_from_group($group);
 355  
 356          // Setup verb.
 357          if (!$validverb) {
 358              $info['verb'] = item_verb::create_from_id('invalid');
 359          }
 360  
 361          $data = $this->get_valid_statement($info);
 362  
 363          if ($multiple) {
 364              $data = [
 365                  $this->get_valid_statement(),
 366                  $data,
 367              ];
 368          }
 369  
 370          // Perform test.
 371          $this->post_statements_data ('fake_component', $data, $expected);
 372      }
 373  
 374      /**
 375       * Data provider for the test_components tests.
 376       *
 377       * @return  array
 378       */
 379      public function statement_provider() : array {
 380          return [
 381              // Single statement with group statements enabled.
 382              'Single, Valid actor, valid verb' => [
 383                  false, true, true, [true]
 384              ],
 385              'Single, Invalid actor, valid verb' => [
 386                  false, false, true, []
 387              ],
 388              'Single, Valid actor, invalid verb' => [
 389                  false, true, false, []
 390              ],
 391              'Single, Inalid actor, invalid verb' => [
 392                  false, false, false, []
 393              ],
 394              // Multi statement with group statements enabled.
 395              'Multiple, Valid actor, valid verb' => [
 396                  true, true, true, [true, true]
 397              ],
 398              'Multiple, Invalid actor, valid verb' => [
 399                  true, false, true, []
 400              ],
 401              'Multiple, Valid actor, invalid verb' => [
 402                  true, true, false, [true, false]
 403              ],
 404              'Multiple, Inalid actor, invalid verb' => [
 405                  true, false, false, []
 406              ],
 407          ];
 408      }
 409  
 410      /**
 411       * Test posting group statements to a handler without group actor support.
 412       *
 413       * Try to use group statement in components that not support this feature
 414       * causes a full statements batch rejection.
 415       *
 416       * @dataProvider group_statement_provider
 417       * @param bool $usegroup1 if the 1st statement must be groupal
 418       * @param bool $usegroup2 if the 2nd statement must be groupal
 419       * @param array $expected expected results
 420       */
 421      public function test_group_disabled(bool $usegroup1, bool $usegroup2, array $expected) {
 422          global $USER;
 423  
 424          $this->resetAfterTest();
 425  
 426          $this->setAdminUser();
 427  
 428          // Create one course and 1 group.
 429          $course = $this->getDataGenerator()->create_course();
 430          $this->getDataGenerator()->enrol_user($USER->id, $course->id);
 431          $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 432          $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $USER->id));
 433  
 434          $info = ['actor' => item_group::create_from_group($group)];
 435  
 436          $groupstatement = $this->get_valid_statement($info);
 437          $agentstatement = $this->get_valid_statement();
 438  
 439          $data = [];
 440          $data[] = ($usegroup1) ? $groupstatement : $agentstatement;
 441          $data[] = ($usegroup2) ? $groupstatement : $agentstatement;
 442  
 443          // Perform test.
 444          $this->post_statements_data ('fake_component', $data, $expected);
 445      }
 446  
 447      /**
 448       * Data provider for the test_components tests.
 449       *
 450       * @return  array
 451       */
 452      public function group_statement_provider() : array {
 453          return [
 454              // Single statement with group statements enabled.
 455              'Group statement + group statement without group support' => [
 456                  true, true, []
 457              ],
 458              'Group statement + agent statement without group support' => [
 459                  true, false, []
 460              ],
 461              'Agent statement + group statement without group support' => [
 462                  true, false, []
 463              ],
 464              'Agent statement + agent statement without group support' => [
 465                  false, false, [true, true]
 466              ],
 467          ];
 468      }
 469  
 470      /**
 471       * Test posting a statements batch not accepted by handler.
 472       *
 473       * If all statements from a batch are rejectes by the plugin the full
 474       * batch is considered rejected and an exception is returned.
 475       */
 476      public function test_full_batch_rejected() {
 477          $this->resetAfterTest();
 478  
 479          $this->setAdminUser();
 480  
 481          $info = ['verb' => item_verb::create_from_id('invalid')];
 482  
 483          $statement = $this->get_valid_statement($info);
 484  
 485          $data = [$statement, $statement];
 486  
 487          // Perform test.
 488          $this->post_statements_data ('fake_component', $data, []);
 489      }
 490  }