Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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   * 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() {
  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->assertContains('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->assertContains('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->assertContains('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->assertContains('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->assertContains('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->assertContains('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->assertContains('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->assertContains('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->assertContains('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          $this->assertRegExp('~Department~', $information);
 317  
 318          // Set the field to true for both users and retry.
 319          $DB->set_field('user', 'department', 'Cheese Studies', array('id' => $user->id));
 320          $USER->department = 'Cheese Studies';
 321          $this->assertTrue($cond->is_available(false, $info, true, $USER->id));
 322          $this->assertTrue($cond->is_available(false, $info, true, $user->id));
 323      }
 324  
 325      /**
 326       * Tests what happens with custom fields that are text areas. These should
 327       * not be offered in the menu because their data is not included in user
 328       * object
 329       */
 330      public function test_custom_textarea_field() {
 331          global $USER, $SITE, $DB;
 332          $this->setAdminUser();
 333          $info = new \core_availability\mock_info();
 334  
 335          // Add custom textarea type.
 336          $DB->insert_record('user_info_field', array(
 337                  'shortname' => 'longtext', 'name' => 'Long text', 'categoryid' => 1,
 338                  'datatype' => 'textarea'));
 339          $customfield = $DB->get_record('user_info_field',
 340                  array('shortname' => 'longtext'));
 341  
 342          // The list of fields should include the text field added in setUp(),
 343          // but should not include the textarea field added just now.
 344          $fields = condition::get_custom_profile_fields();
 345          $this->assertArrayHasKey('frogtype', $fields);
 346          $this->assertArrayNotHasKey('longtext', $fields);
 347      }
 348  
 349      /**
 350       * Sets the custom profile field used for testing.
 351       *
 352       * @param int $userid User id
 353       * @param string|null $value Field value or null to clear
 354       * @param int $fieldid Field id or 0 to use default one
 355       */
 356      protected function set_field($userid, $value, $fieldid = 0) {
 357          global $DB, $USER;
 358  
 359          if (!$fieldid) {
 360              $fieldid = $this->profilefield->id;
 361          }
 362          $alreadyset = array_key_exists($userid, $this->setusers);
 363          if (is_null($value)) {
 364              $DB->delete_records('user_info_data',
 365                      array('userid' => $userid, 'fieldid' => $fieldid));
 366              unset($this->setusers[$userid]);
 367          } else if ($alreadyset) {
 368              $DB->set_field('user_info_data', 'data', $value,
 369                      array('userid' => $userid, 'fieldid' => $fieldid));
 370          } else {
 371              $DB->insert_record('user_info_data', array('userid' => $userid,
 372                      'fieldid' => $fieldid, 'data' => $value));
 373              $this->setusers[$userid] = true;
 374          }
 375      }
 376  
 377      /**
 378       * Checks the result of is_available. This function is to save duplicated
 379       * code; it does two checks (the normal is_available with $not set to true
 380       * and set to false). Whichever result is expected to be true, it checks
 381       * $information ends up as empty string for that one, and as a regex match
 382       * for another one.
 383       *
 384       * @param bool $yes If the positive test is expected to return true
 385       * @param string $failpattern Regex pattern to match text when it returns false
 386       * @param condition $cond Condition
 387       * @param \core_availability\info $info Information about current context
 388       * @param int $userid User id
 389       */
 390      protected function assert_is_available_result($yes, $failpattern, condition $cond,
 391              \core_availability\info $info, $userid) {
 392          // Positive (normal) test.
 393          $this->assertEquals($yes, $cond->is_available(false, $info, true, $userid),
 394                  'Failed checking normal (positive) result');
 395          if (!$yes) {
 396              $information = $cond->get_description(false, false, $info);
 397              $this->assertRegExp($failpattern, $information);
 398          }
 399  
 400          // Negative (NOT) test.
 401          $this->assertEquals(!$yes, $cond->is_available(true, $info, true, $userid),
 402                  'Failed checking NOT (negative) result');
 403          if ($yes) {
 404              $information = $cond->get_description(false, true, $info);
 405              $this->assertRegExp($failpattern, $information);
 406          }
 407      }
 408  
 409      /**
 410       * Tests the filter_users (bulk checking) function.
 411       */
 412      public function test_filter_users() {
 413          global $DB, $CFG;
 414          $this->resetAfterTest();
 415          $CFG->enableavailability = true;
 416  
 417          // Erase static cache before test.
 418          condition::wipe_static_cache();
 419  
 420          // Make a test course and some users.
 421          $generator = $this->getDataGenerator();
 422          $course = $generator->create_course();
 423          $student1 = $generator->create_user(array('institution' => 'Unseen University'));
 424          $student2 = $generator->create_user(array('institution' => 'Hogwarts'));
 425          $student3 = $generator->create_user(array('institution' => 'Unseen University'));
 426          $allusers = array();
 427          foreach (array($student1, $student2, $student3) as $student) {
 428              $generator->enrol_user($student->id, $course->id);
 429              $allusers[$student->id] = $student;
 430          }
 431          $this->set_field($student1->id, 'poison dart');
 432          $this->set_field($student2->id, 'poison dart');
 433          $info = new \core_availability\mock_info($course);
 434          $checker = new \core_availability\capability_checker($info->get_context());
 435  
 436          // Test standard field condition (positive and negative).
 437          $cond = new condition((object)array('sf' => 'institution', 'op' => 'contains', 'v' => 'Unseen'));
 438          $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
 439          ksort($result);
 440          $this->assertEquals(array($student1->id, $student3->id), $result);
 441          $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
 442          ksort($result);
 443          $this->assertEquals(array($student2->id), $result);
 444  
 445          // Test custom field condition.
 446          $cond = new condition((object)array('cf' => 'frogtype', 'op' => 'contains', 'v' => 'poison'));
 447          $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
 448          ksort($result);
 449          $this->assertEquals(array($student1->id, $student2->id), $result);
 450          $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
 451          ksort($result);
 452          $this->assertEquals(array($student3->id), $result);
 453      }
 454  
 455      /**
 456       * Tests getting user list SQL. This is a different test from the above because
 457       * there is some additional code in this function so more variants need testing.
 458       */
 459      public function test_get_user_list_sql() {
 460          global $DB, $CFG;
 461          $this->resetAfterTest();
 462          $CFG->enableavailability = true;
 463  
 464          // Erase static cache before test.
 465          condition::wipe_static_cache();
 466  
 467          // For testing, make another info field with default value.
 468          $DB->insert_record('user_info_field', array(
 469                  'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'categoryid' => 1,
 470                  'datatype' => 'text', 'defaultdata' => 'Slimy'));
 471          $otherprofilefield = $DB->get_record('user_info_field',
 472                  array('shortname' => 'tonguestyle'));
 473  
 474          // Make a test course and some users.
 475          $generator = $this->getDataGenerator();
 476          $course = $generator->create_course();
 477          $student1 = $generator->create_user(array('institution' => 'Unseen University'));
 478          $student2 = $generator->create_user(array('institution' => 'Hogwarts'));
 479          $student3 = $generator->create_user(array('institution' => 'Unseen University'));
 480          $student4 = $generator->create_user(array('institution' => '0'));
 481          $allusers = array();
 482          foreach (array($student1, $student2, $student3, $student4) as $student) {
 483              $generator->enrol_user($student->id, $course->id);
 484              $allusers[$student->id] = $student;
 485          }
 486          $this->set_field($student1->id, 'poison dart');
 487          $this->set_field($student2->id, 'poison dart');
 488          $this->set_field($student3->id, 'Rough', $otherprofilefield->id);
 489          $this->info = new \core_availability\mock_info($course);
 490  
 491          // Test standard field condition (positive).
 492          $this->cond = new condition((object)array('sf' => 'institution',
 493                  'op' => condition::OP_CONTAINS, 'v' => 'Univ'));
 494          $this->assert_user_list_sql_results(array($student1->id, $student3->id));
 495  
 496          // Now try it negative.
 497          $this->assert_user_list_sql_results(array($student2->id, $student4->id), true);
 498  
 499          // Try all the other condition types.
 500          $this->cond = new condition((object)array('sf' => 'institution',
 501                  'op' => condition::OP_DOES_NOT_CONTAIN, 'v' => 's'));
 502          $this->assert_user_list_sql_results(array($student4->id));
 503          $this->cond = new condition((object)array('sf' => 'institution',
 504                  'op' => condition::OP_IS_EQUAL_TO, 'v' => 'Hogwarts'));
 505          $this->assert_user_list_sql_results(array($student2->id));
 506          $this->cond = new condition((object)array('sf' => 'institution',
 507                  'op' => condition::OP_STARTS_WITH, 'v' => 'U'));
 508          $this->assert_user_list_sql_results(array($student1->id, $student3->id));
 509          $this->cond = new condition((object)array('sf' => 'institution',
 510                  'op' => condition::OP_ENDS_WITH, 'v' => 'rts'));
 511          $this->assert_user_list_sql_results(array($student2->id));
 512          $this->cond = new condition((object)array('sf' => 'institution',
 513                  'op' => condition::OP_IS_EMPTY));
 514          $this->assert_user_list_sql_results(array($student4->id));
 515          $this->cond = new condition((object)array('sf' => 'institution',
 516                  'op' => condition::OP_IS_NOT_EMPTY));
 517          $this->assert_user_list_sql_results(array($student1->id, $student2->id, $student3->id));
 518  
 519          // Try with a custom field condition that doesn't have a default.
 520          $this->cond = new condition((object)array('cf' => 'frogtype',
 521                  'op' => condition::OP_CONTAINS, 'v' => 'poison'));
 522          $this->assert_user_list_sql_results(array($student1->id, $student2->id));
 523          $this->cond = new condition((object)array('cf' => 'frogtype',
 524                  'op' => condition::OP_IS_EMPTY));
 525          $this->assert_user_list_sql_results(array($student3->id, $student4->id));
 526  
 527          // Try with one that does have a default.
 528          $this->cond = new condition((object)array('cf' => 'tonguestyle',
 529                  'op' => condition::OP_STARTS_WITH, 'v' => 'Sli'));
 530          $this->assert_user_list_sql_results(array($student1->id, $student2->id,
 531                  $student4->id));
 532          $this->cond = new condition((object)array('cf' => 'tonguestyle',
 533                  'op' => condition::OP_IS_EMPTY));
 534          $this->assert_user_list_sql_results(array());
 535      }
 536  
 537      /**
 538       * Convenience function. Gets the user list SQL and runs it, then checks
 539       * results.
 540       *
 541       * @param array $expected Array of expected user ids
 542       * @param bool $not True if using NOT condition
 543       */
 544      private function assert_user_list_sql_results(array $expected, $not = false) {
 545          global $DB;
 546          list ($sql, $params) = $this->cond->get_user_list_sql($not, $this->info, true);
 547          $result = $DB->get_fieldset_sql($sql, $params);
 548          sort($result);
 549          $this->assertEquals($expected, $result);
 550      }
 551  }