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