Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Unit tests for the user profile condition.
  19   *
  20   * @package availability_profile
  21   * @copyright 2014 The Open University
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  use availability_profile\condition;
  28  
  29  /**
  30   * Unit tests for the user profile condition.
  31   *
  32   * @package availability_profile
  33   * @copyright 2014 The Open University
  34   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class availability_profile_condition_testcase extends advanced_testcase {
  37      /** @var profile_define_text Profile field for testing */
  38      protected $profilefield;
  39  
  40      /** @var array Array of user IDs for whome we already set the profile field */
  41      protected $setusers = array();
  42  
  43      /** @var condition Current condition */
  44      private $cond;
  45      /** @var \core_availability\info Current info */
  46      private $info;
  47  
  48      public function setUp(): void {
  49          global $DB, $CFG;
  50  
  51          $this->resetAfterTest();
  52  
  53          // Add a custom profile field type. The API for doing this is indescribably
  54          // horrid and tightly intertwined with the form UI, so it's best to add
  55          // it directly in database.
  56          $DB->insert_record('user_info_field', array(
  57                  'shortname' => 'frogtype', 'name' => 'Type of frog', 'categoryid' => 1,
  58                  'datatype' => 'text'));
  59          $this->profilefield = $DB->get_record('user_info_field',
  60                  array('shortname' => 'frogtype'));
  61  
  62          // Clear static cache.
  63          \availability_profile\condition::wipe_static_cache();
  64  
  65          // Load the mock info class so that it can be used.
  66          require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
  67      }
  68  
  69      /**
  70       * Tests constructing and using date condition as part of tree.
  71       */
  72      public function test_in_tree() {
  73          global $USER;
  74  
  75          $this->setAdminUser();
  76  
  77          $info = new \core_availability\mock_info();
  78  
  79          $structure = (object)array('op' => '|', 'show' => true, 'c' => array(
  80                  (object)array('type' => 'profile',
  81                          'op' => condition::OP_IS_EQUAL_TO,
  82                          'cf' => 'frogtype', 'v' => 'tree')));
  83          $tree = new \core_availability\tree($structure);
  84  
  85          // Initial check (user does not have custom field).
  86          $result = $tree->check_available(false, $info, true, $USER->id);
  87          $this->assertFalse($result->is_available());
  88  
  89          // Set field.
  90          $this->set_field($USER->id, 'tree');
  91  
  92          // Now it's true!
  93          $result = $tree->check_available(false, $info, true, $USER->id);
  94          $this->assertTrue($result->is_available());
  95      }
  96  
  97      /**
  98       * Tests the constructor including error conditions. Also tests the
  99       * string conversion feature (intended for debugging only).
 100       */
 101      public function test_constructor() {
 102          // No parameters.
 103          $structure = new stdClass();
 104          try {
 105              $cond = new condition($structure);
 106              $this->fail();
 107          } catch (coding_exception $e) {
 108              $this->assertStringContainsString('Missing or invalid ->op', $e->getMessage());
 109          }
 110  
 111          // Invalid op.
 112          $structure->op = 'isklingonfor';
 113          try {
 114              $cond = new condition($structure);
 115              $this->fail();
 116          } catch (coding_exception $e) {
 117              $this->assertStringContainsString('Missing or invalid ->op', $e->getMessage());
 118          }
 119  
 120          // Missing value.
 121          $structure->op = condition::OP_IS_EQUAL_TO;
 122          try {
 123              $cond = new condition($structure);
 124              $this->fail();
 125          } catch (coding_exception $e) {
 126              $this->assertStringContainsString('Missing or invalid ->v', $e->getMessage());
 127          }
 128  
 129          // Invalid value (not string).
 130          $structure->v = false;
 131          try {
 132              $cond = new condition($structure);
 133              $this->fail();
 134          } catch (coding_exception $e) {
 135              $this->assertStringContainsString('Missing or invalid ->v', $e->getMessage());
 136          }
 137  
 138          // Unexpected value.
 139          $structure->op = condition::OP_IS_EMPTY;
 140          try {
 141              $cond = new condition($structure);
 142              $this->fail();
 143          } catch (coding_exception $e) {
 144              $this->assertStringContainsString('Unexpected ->v', $e->getMessage());
 145          }
 146  
 147          // Missing field.
 148          $structure->op = condition::OP_IS_EQUAL_TO;
 149          $structure->v = 'flying';
 150          try {
 151              $cond = new condition($structure);
 152              $this->fail();
 153          } catch (coding_exception $e) {
 154              $this->assertStringContainsString('Missing ->sf or ->cf', $e->getMessage());
 155          }
 156  
 157          // Invalid field (not string).
 158          $structure->sf = 42;
 159          try {
 160              $cond = new condition($structure);
 161              $this->fail();
 162          } catch (coding_exception $e) {
 163              $this->assertStringContainsString('Invalid ->sf', $e->getMessage());
 164          }
 165  
 166          // Both fields.
 167          $structure->sf = 'department';
 168          $structure->cf = 'frogtype';
 169          try {
 170              $cond = new condition($structure);
 171              $this->fail();
 172          } catch (coding_exception $e) {
 173              $this->assertStringContainsString('Both ->sf and ->cf', $e->getMessage());
 174          }
 175  
 176          // Invalid ->cf field (not string).
 177          unset($structure->sf);
 178          $structure->cf = false;
 179          try {
 180              $cond = new condition($structure);
 181              $this->fail();
 182          } catch (coding_exception $e) {
 183              $this->assertStringContainsString('Invalid ->cf', $e->getMessage());
 184          }
 185  
 186          // Valid examples (checks values are correctly included).
 187          $structure->cf = 'frogtype';
 188          $cond = new condition($structure);
 189          $this->assertEquals('{profile:*frogtype isequalto flying}', (string)$cond);
 190  
 191          unset($structure->v);
 192          $structure->op = condition::OP_IS_EMPTY;
 193          $cond = new condition($structure);
 194          $this->assertEquals('{profile:*frogtype isempty}', (string)$cond);
 195  
 196          unset($structure->cf);
 197          $structure->sf = 'department';
 198          $cond = new condition($structure);
 199          $this->assertEquals('{profile:department isempty}', (string)$cond);
 200      }
 201  
 202      /**
 203       * Tests the save() function.
 204       */
 205      public function test_save() {
 206          $structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_IS_EMPTY);
 207          $cond = new condition($structure);
 208          $structure->type = 'profile';
 209          $this->assertEquals($structure, $cond->save());
 210  
 211          $structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_ENDS_WITH,
 212                  'v' => 'bouncy');
 213          $cond = new condition($structure);
 214          $structure->type = 'profile';
 215          $this->assertEquals($structure, $cond->save());
 216      }
 217  
 218      /**
 219       * Tests the is_available function. There is no separate test for
 220       * get_full_information because that function is called from is_available
 221       * and we test its values here.
 222       */
 223      public function test_is_available() {
 224          global $USER, $SITE, $DB;
 225          $this->setAdminUser();
 226          $info = new \core_availability\mock_info();
 227  
 228          // Prepare to test with all operators against custom field using all
 229          // combinations of NOT and true/false states..
 230          $information = 'x';
 231          $structure = (object)array('cf' => 'frogtype');
 232  
 233          $structure->op = condition::OP_IS_NOT_EMPTY;
 234          $cond = new condition($structure);
 235          $this->assert_is_available_result(false, '~Type of frog.*is not empty~',
 236                  $cond, $info, $USER->id);
 237          $this->set_field($USER->id, 'poison dart');
 238          $this->assert_is_available_result(true, '~Type of frog.*is empty~',
 239                  $cond, $info, $USER->id);
 240  
 241          $structure->op = condition::OP_IS_EMPTY;
 242          $cond = new condition($structure);
 243          $this->assert_is_available_result(false, '~.*Type of frog.*is empty~',
 244                  $cond, $info, $USER->id);
 245          $this->set_field($USER->id, null);
 246          $this->assert_is_available_result(true, '~.*Type of frog.*is not empty~',
 247                  $cond, $info, $USER->id);
 248          $this->set_field($USER->id, '');
 249          $this->assert_is_available_result(true, '~.*Type of frog.*is not empty~',
 250                  $cond, $info, $USER->id);
 251  
 252          $structure->op = condition::OP_CONTAINS;
 253          $structure->v = 'llf';
 254          $cond = new condition($structure);
 255          $this->assert_is_available_result(false, '~Type of frog.*contains.*llf~',
 256                  $cond, $info, $USER->id);
 257          $this->set_field($USER->id, 'bullfrog');
 258          $this->assert_is_available_result(true, '~Type of frog.*does not contain.*llf~',
 259                  $cond, $info, $USER->id);
 260  
 261          $structure->op = condition::OP_DOES_NOT_CONTAIN;
 262          $cond = new condition($structure);
 263          $this->assert_is_available_result(false, '~Type of frog.*does not contain.*llf~',
 264                  $cond, $info, $USER->id);
 265          $this->set_field($USER->id, 'goliath');
 266          $this->assert_is_available_result(true, '~Type of frog.*contains.*llf~',
 267                  $cond, $info, $USER->id);
 268  
 269          $structure->op = condition::OP_IS_EQUAL_TO;
 270          $structure->v = 'Kermit';
 271          $cond = new condition($structure);
 272          $this->assert_is_available_result(false, '~Type of frog.*is <.*Kermit~',
 273                  $cond, $info, $USER->id);
 274          $this->set_field($USER->id, 'Kermit');
 275          $this->assert_is_available_result(true, '~Type of frog.*is not.*Kermit~',
 276                  $cond, $info, $USER->id);
 277  
 278          $structure->op = condition::OP_STARTS_WITH;
 279          $structure->v = 'Kerm';
 280          $cond = new condition($structure);
 281          $this->assert_is_available_result(true, '~Type of frog.*does not start.*Kerm~',
 282                  $cond, $info, $USER->id);
 283          $this->set_field($USER->id, 'Keroppi');
 284          $this->assert_is_available_result(false, '~Type of frog.*starts.*Kerm~',
 285                  $cond, $info, $USER->id);
 286  
 287          $structure->op = condition::OP_ENDS_WITH;
 288          $structure->v = 'ppi';
 289          $cond = new condition($structure);
 290          $this->assert_is_available_result(true, '~Type of frog.*does not end.*ppi~',
 291                  $cond, $info, $USER->id);
 292          $this->set_field($USER->id, 'Kermit');
 293          $this->assert_is_available_result(false, '~Type of frog.*ends.*ppi~',
 294                  $cond, $info, $USER->id);
 295  
 296          // Also test is_available for a different (not current) user.
 297          $generator = $this->getDataGenerator();
 298          $user = $generator->create_user();
 299          $structure->op = condition::OP_CONTAINS;
 300          $structure->v = 'rne';
 301          $cond = new condition($structure);
 302          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 303          $this->set_field($user->id, 'horned');
 304          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 305  
 306          // Now check with a standard field (department).
 307          $structure = (object)array('op' => condition::OP_IS_EQUAL_TO,
 308                  'sf' => 'department', 'v' => 'Cheese Studies');
 309          $cond = new condition($structure);
 310          $this->assertFalse($cond->is_available(false, $info, true, $USER->id));
 311          $this->assertFalse($cond->is_available(false, $info, true, $user->id));
 312  
 313          // Check the message (should be using lang string with capital, which
 314          // is evidence that it called the right function to get the name).
 315          $information = $cond->get_description(false, false, $info);
 316          $information = \core_availability\info::format_info($information, $info->get_course());
 317          $this->assertRegExp('~Department~', $information);
 318  
 319          // Set the field to true for both users and retry.
 320          $DB->set_field('user', 'department', 'Cheese Studies', array('id' => $user->id));
 321          $USER->department = 'Cheese Studies';
 322          $this->assertTrue($cond->is_available(false, $info, true, $USER->id));
 323          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 324      }
 325  
 326      /**
 327       * Tests what happens with custom fields that are text areas. These should
 328       * not be offered in the menu because their data is not included in user
 329       * object
 330       */
 331      public function test_custom_textarea_field() {
 332          global $USER, $SITE, $DB;
 333          $this->setAdminUser();
 334          $info = new \core_availability\mock_info();
 335  
 336          // Add custom textarea type.
 337          $DB->insert_record('user_info_field', array(
 338                  'shortname' => 'longtext', 'name' => 'Long text', 'categoryid' => 1,
 339                  'datatype' => 'textarea'));
 340          $customfield = $DB->get_record('user_info_field',
 341                  array('shortname' => 'longtext'));
 342  
 343          // The list of fields should include the text field added in setUp(),
 344          // but should not include the textarea field added just now.
 345          $fields = condition::get_custom_profile_fields();
 346          $this->assertArrayHasKey('frogtype', $fields);
 347          $this->assertArrayNotHasKey('longtext', $fields);
 348      }
 349  
 350      /**
 351       * Sets the custom profile field used for testing.
 352       *
 353       * @param int $userid User id
 354       * @param string|null $value Field value or null to clear
 355       * @param int $fieldid Field id or 0 to use default one
 356       */
 357      protected function set_field($userid, $value, $fieldid = 0) {
 358          global $DB, $USER;
 359  
 360          if (!$fieldid) {
 361              $fieldid = $this->profilefield->id;
 362          }
 363          $alreadyset = array_key_exists($userid, $this->setusers);
 364          if (is_null($value)) {
 365              $DB->delete_records('user_info_data',
 366                      array('userid' => $userid, 'fieldid' => $fieldid));
 367              unset($this->setusers[$userid]);
 368          } else if ($alreadyset) {
 369              $DB->set_field('user_info_data', 'data', $value,
 370                      array('userid' => $userid, 'fieldid' => $fieldid));
 371          } else {
 372              $DB->insert_record('user_info_data', array('userid' => $userid,
 373                      'fieldid' => $fieldid, 'data' => $value));
 374              $this->setusers[$userid] = true;
 375          }
 376      }
 377  
 378      /**
 379       * Checks the result of is_available. This function is to save duplicated
 380       * code; it does two checks (the normal is_available with $not set to true
 381       * and set to false). Whichever result is expected to be true, it checks
 382       * $information ends up as empty string for that one, and as a regex match
 383       * for another one.
 384       *
 385       * @param bool $yes If the positive test is expected to return true
 386       * @param string $failpattern Regex pattern to match text when it returns false
 387       * @param condition $cond Condition
 388       * @param \core_availability\info $info Information about current context
 389       * @param int $userid User id
 390       */
 391      protected function assert_is_available_result($yes, $failpattern, condition $cond,
 392              \core_availability\info $info, $userid) {
 393          // Positive (normal) test.
 394          $this->assertEquals($yes, $cond->is_available(false, $info, true, $userid),
 395                  'Failed checking normal (positive) result');
 396          if (!$yes) {
 397              $information = $cond->get_description(false, false, $info);
 398              $information = \core_availability\info::format_info($information, $info->get_course());
 399              $this->assertRegExp($failpattern, $information);
 400          }
 401  
 402          // Negative (NOT) test.
 403          $this->assertEquals(!$yes, $cond->is_available(true, $info, true, $userid),
 404                  'Failed checking NOT (negative) result');
 405          if ($yes) {
 406              $information = $cond->get_description(false, true, $info);
 407              $information = \core_availability\info::format_info($information, $info->get_course());
 408              $this->assertRegExp($failpattern, $information);
 409          }
 410      }
 411  
 412      /**
 413       * Tests the filter_users (bulk checking) function.
 414       */
 415      public function test_filter_users() {
 416          global $DB, $CFG;
 417          $this->resetAfterTest();
 418          $CFG->enableavailability = true;
 419  
 420          // Erase static cache before test.
 421          condition::wipe_static_cache();
 422  
 423          // Make a test course and some users.
 424          $generator = $this->getDataGenerator();
 425          $course = $generator->create_course();
 426          $student1 = $generator->create_user(array('institution' => 'Unseen University'));
 427          $student2 = $generator->create_user(array('institution' => 'Hogwarts'));
 428          $student3 = $generator->create_user(array('institution' => 'Unseen University'));
 429          $allusers = array();
 430          foreach (array($student1, $student2, $student3) as $student) {
 431              $generator->enrol_user($student->id, $course->id);
 432              $allusers[$student->id] = $student;
 433          }
 434          $this->set_field($student1->id, 'poison dart');
 435          $this->set_field($student2->id, 'poison dart');
 436          $info = new \core_availability\mock_info($course);
 437          $checker = new \core_availability\capability_checker($info->get_context());
 438  
 439          // Test standard field condition (positive and negative).
 440          $cond = new condition((object)array('sf' => 'institution', 'op' => 'contains', 'v' => 'Unseen'));
 441          $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
 442          ksort($result);
 443          $this->assertEquals(array($student1->id, $student3->id), $result);
 444          $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
 445          ksort($result);
 446          $this->assertEquals(array($student2->id), $result);
 447  
 448          // Test custom field condition.
 449          $cond = new condition((object)array('cf' => 'frogtype', 'op' => 'contains', 'v' => 'poison'));
 450          $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
 451          ksort($result);
 452          $this->assertEquals(array($student1->id, $student2->id), $result);
 453          $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
 454          ksort($result);
 455          $this->assertEquals(array($student3->id), $result);
 456      }
 457  
 458      /**
 459       * Tests getting user list SQL. This is a different test from the above because
 460       * there is some additional code in this function so more variants need testing.
 461       */
 462      public function test_get_user_list_sql() {
 463          global $DB, $CFG;
 464          $this->resetAfterTest();
 465          $CFG->enableavailability = true;
 466  
 467          // Erase static cache before test.
 468          condition::wipe_static_cache();
 469  
 470          // For testing, make another info field with default value.
 471          $DB->insert_record('user_info_field', array(
 472                  'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'categoryid' => 1,
 473                  'datatype' => 'text', 'defaultdata' => 'Slimy'));
 474          $otherprofilefield = $DB->get_record('user_info_field',
 475                  array('shortname' => 'tonguestyle'));
 476  
 477          // Make a test course and some users.
 478          $generator = $this->getDataGenerator();
 479          $course = $generator->create_course();
 480          $student1 = $generator->create_user(array('institution' => 'Unseen University'));
 481          $student2 = $generator->create_user(array('institution' => 'Hogwarts'));
 482          $student3 = $generator->create_user(array('institution' => 'Unseen University'));
 483          $student4 = $generator->create_user(array('institution' => '0'));
 484          $allusers = array();
 485          foreach (array($student1, $student2, $student3, $student4) as $student) {
 486              $generator->enrol_user($student->id, $course->id);
 487              $allusers[$student->id] = $student;
 488          }
 489          $this->set_field($student1->id, 'poison dart');
 490          $this->set_field($student2->id, 'poison dart');
 491          $this->set_field($student3->id, 'Rough', $otherprofilefield->id);
 492          $this->info = new \core_availability\mock_info($course);
 493  
 494          // Test standard field condition (positive).
 495          $this->cond = new condition((object)array('sf' => 'institution',
 496                  'op' => condition::OP_CONTAINS, 'v' => 'Univ'));
 497          $this->assert_user_list_sql_results(array($student1->id, $student3->id));
 498  
 499          // Now try it negative.
 500          $this->assert_user_list_sql_results(array($student2->id, $student4->id), true);
 501  
 502          // Try all the other condition types.
 503          $this->cond = new condition((object)array('sf' => 'institution',
 504                  'op' => condition::OP_DOES_NOT_CONTAIN, 'v' => 's'));
 505          $this->assert_user_list_sql_results(array($student4->id));
 506          $this->cond = new condition((object)array('sf' => 'institution',
 507                  'op' => condition::OP_IS_EQUAL_TO, 'v' => 'Hogwarts'));
 508          $this->assert_user_list_sql_results(array($student2->id));
 509          $this->cond = new condition((object)array('sf' => 'institution',
 510                  'op' => condition::OP_STARTS_WITH, 'v' => 'U'));
 511          $this->assert_user_list_sql_results(array($student1->id, $student3->id));
 512          $this->cond = new condition((object)array('sf' => 'institution',
 513                  'op' => condition::OP_ENDS_WITH, 'v' => 'rts'));
 514          $this->assert_user_list_sql_results(array($student2->id));
 515          $this->cond = new condition((object)array('sf' => 'institution',
 516                  'op' => condition::OP_IS_EMPTY));
 517          $this->assert_user_list_sql_results(array($student4->id));
 518          $this->cond = new condition((object)array('sf' => 'institution',
 519                  'op' => condition::OP_IS_NOT_EMPTY));
 520          $this->assert_user_list_sql_results(array($student1->id, $student2->id, $student3->id));
 521  
 522          // Try with a custom field condition that doesn't have a default.
 523          $this->cond = new condition((object)array('cf' => 'frogtype',
 524                  'op' => condition::OP_CONTAINS, 'v' => 'poison'));
 525          $this->assert_user_list_sql_results(array($student1->id, $student2->id));
 526          $this->cond = new condition((object)array('cf' => 'frogtype',
 527                  'op' => condition::OP_IS_EMPTY));
 528          $this->assert_user_list_sql_results(array($student3->id, $student4->id));
 529  
 530          // Try with one that does have a default.
 531          $this->cond = new condition((object)array('cf' => 'tonguestyle',
 532                  'op' => condition::OP_STARTS_WITH, 'v' => 'Sli'));
 533          $this->assert_user_list_sql_results(array($student1->id, $student2->id,
 534                  $student4->id));
 535          $this->cond = new condition((object)array('cf' => 'tonguestyle',
 536                  'op' => condition::OP_IS_EMPTY));
 537          $this->assert_user_list_sql_results(array());
 538      }
 539  
 540      /**
 541       * Convenience function. Gets the user list SQL and runs it, then checks
 542       * results.
 543       *
 544       * @param array $expected Array of expected user ids
 545       * @param bool $not True if using NOT condition
 546       */
 547      private function assert_user_list_sql_results(array $expected, $not = false) {
 548          global $DB;
 549          list ($sql, $params) = $this->cond->get_user_list_sql($not, $this->info, true);
 550          $result = $DB->get_fieldset_sql($sql, $params);
 551          sort($result);
 552          $this->assertEquals($expected, $result);
 553      }
 554  }