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] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core;
  18  
  19  /**
  20   * Test for various bits of datalib.php.
  21   *
  22   * @package   core
  23   * @category  test
  24   * @copyright 2012 The Open University
  25   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  class datalib_test extends \advanced_testcase {
  28      protected function normalise_sql($sort) {
  29          return preg_replace('~\s+~', ' ', $sort);
  30      }
  31  
  32      protected function assert_same_sql($expected, $actual) {
  33          $this->assertSame($this->normalise_sql($expected), $this->normalise_sql($actual));
  34      }
  35  
  36      /**
  37       * Do a test of the user search SQL with database users.
  38       */
  39      public function test_users_search_sql() {
  40          global $DB;
  41          $this->resetAfterTest();
  42  
  43          // Set up test users.
  44          $user1 = array(
  45              'username' => 'usernametest1',
  46              'idnumber' => 'idnumbertest1',
  47              'firstname' => 'First Name User Test 1',
  48              'lastname' => 'Last Name User Test 1',
  49              'email' => 'usertest1@example.com',
  50              'address' => '2 Test Street Perth 6000 WA',
  51              'phone1' => '01010101010',
  52              'phone2' => '02020203',
  53              'department' => 'Department of user 1',
  54              'institution' => 'Institution of user 1',
  55              'description' => 'This is a description for user 1',
  56              'descriptionformat' => FORMAT_MOODLE,
  57              'city' => 'Perth',
  58              'country' => 'AU'
  59              );
  60          $user1 = self::getDataGenerator()->create_user($user1);
  61          $user2 = array(
  62              'username' => 'usernametest2',
  63              'idnumber' => 'idnumbertest2',
  64              'firstname' => 'First Name User Test 2',
  65              'lastname' => 'Last Name User Test 2',
  66              'email' => 'usertest2@example.com',
  67              'address' => '222 Test Street Perth 6000 WA',
  68              'phone1' => '01010101010',
  69              'phone2' => '02020203',
  70              'department' => 'Department of user 2',
  71              'institution' => 'Institution of user 2',
  72              'description' => 'This is a description for user 2',
  73              'descriptionformat' => FORMAT_MOODLE,
  74              'city' => 'Perth',
  75              'country' => 'AU'
  76              );
  77          $user2 = self::getDataGenerator()->create_user($user2);
  78  
  79          // Search by name (anywhere in text).
  80          list($sql, $params) = users_search_sql('User Test 2', '', USER_SEARCH_CONTAINS);
  81          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
  82          $this->assertFalse(array_key_exists($user1->id, $results));
  83          $this->assertTrue(array_key_exists($user2->id, $results));
  84  
  85          // Search by (most of) full name.
  86          list($sql, $params) = users_search_sql('First Name User Test 2 Last Name User', '', USER_SEARCH_CONTAINS);
  87          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
  88          $this->assertFalse(array_key_exists($user1->id, $results));
  89          $this->assertTrue(array_key_exists($user2->id, $results));
  90  
  91          // Search by name (start of text) valid or not.
  92          list($sql, $params) = users_search_sql('User Test 2', '');
  93          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
  94          $this->assertEquals(0, count($results));
  95          list($sql, $params) = users_search_sql('First Name User Test 2', '');
  96          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
  97          $this->assertFalse(array_key_exists($user1->id, $results));
  98          $this->assertTrue(array_key_exists($user2->id, $results));
  99  
 100          // Search by extra fields included or not (address).
 101          list($sql, $params) = users_search_sql('Test Street', '', USER_SEARCH_CONTAINS);
 102          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
 103          $this->assertCount(0, $results);
 104          list($sql, $params) = users_search_sql('Test Street', '', USER_SEARCH_CONTAINS, array('address'));
 105          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
 106          $this->assertCount(2, $results);
 107  
 108          // Exclude user.
 109          list($sql, $params) = users_search_sql('User Test', '', USER_SEARCH_CONTAINS, array(), array($user1->id));
 110          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
 111          $this->assertFalse(array_key_exists($user1->id, $results));
 112          $this->assertTrue(array_key_exists($user2->id, $results));
 113  
 114          // Include only user.
 115          list($sql, $params) = users_search_sql('User Test', '', USER_SEARCH_CONTAINS, array(), array(), array($user1->id));
 116          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
 117          $this->assertTrue(array_key_exists($user1->id, $results));
 118          $this->assertFalse(array_key_exists($user2->id, $results));
 119  
 120          // Exact match only.
 121          [$sql, $params] = users_search_sql('Last Name User Test 1', '', USER_SEARCH_EXACT_MATCH, [], null, null, true);
 122          $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
 123          $this->assertTrue(array_key_exists($user1->id, $results));
 124          $this->assertFalse(array_key_exists($user2->id, $results));
 125  
 126          // Join with another table and use different prefix.
 127          set_user_preference('amphibian', 'frog', $user1);
 128          set_user_preference('amphibian', 'salamander', $user2);
 129          list($sql, $params) = users_search_sql('User Test 1', 'qq', USER_SEARCH_CONTAINS);
 130          $results = $DB->get_records_sql("
 131                  SELECT up.id, up.value
 132                    FROM {user} qq
 133                    JOIN {user_preferences} up ON up.userid = qq.id
 134                   WHERE up.name = :prefname
 135                         AND $sql", array_merge(array('prefname' => 'amphibian'), $params));
 136          $this->assertEquals(1, count($results));
 137          foreach ($results as $record) {
 138              $this->assertSame('frog', $record->value);
 139          }
 140  
 141          // Join with another table and include other table fields in search.
 142          set_user_preference('reptile', 'snake', $user1);
 143          set_user_preference('reptile', 'lizard', $user2);
 144          list($sql, $params) = users_search_sql('snake', 'qq', USER_SEARCH_CONTAINS, ['up.value']);
 145          $results = $DB->get_records_sql("
 146                  SELECT up.id, up.value
 147                    FROM {user} qq
 148                    JOIN {user_preferences} up ON up.userid = qq.id
 149                   WHERE up.name = :prefname
 150                         AND $sql", array_merge(array('prefname' => 'reptile'), $params));
 151          $this->assertEquals(1, count($results));
 152          foreach ($results as $record) {
 153              $this->assertSame('snake', $record->value);
 154          }
 155      }
 156  
 157      public function test_users_order_by_sql_simple() {
 158          list($sort, $params) = users_order_by_sql();
 159          $this->assert_same_sql('lastname, firstname, id', $sort);
 160          $this->assertEquals(array(), $params);
 161      }
 162  
 163      public function test_users_order_by_sql_table_prefix() {
 164          list($sort, $params) = users_order_by_sql('u');
 165          $this->assert_same_sql('u.lastname, u.firstname, u.id', $sort);
 166          $this->assertEquals(array(), $params);
 167      }
 168  
 169      public function test_users_order_by_sql_search_no_extra_fields() {
 170          global $CFG, $DB;
 171          $this->resetAfterTest(true);
 172  
 173          $CFG->showuseridentity = '';
 174  
 175          list($sort, $params) = users_order_by_sql('', 'search', \context_system::instance());
 176          $this->assert_same_sql('CASE WHEN
 177                      ' . $DB->sql_fullname() . ' = :usersortexact1 OR
 178                      LOWER(firstname) = LOWER(:usersortexact2) OR
 179                      LOWER(lastname) = LOWER(:usersortexact3)
 180                  THEN 0 ELSE 1 END, lastname, firstname, id', $sort);
 181          $this->assertEquals(array('usersortexact1' => 'search', 'usersortexact2' => 'search',
 182                  'usersortexact3' => 'search'), $params);
 183      }
 184  
 185      public function test_users_order_by_sql_search_with_extra_fields_and_prefix() {
 186          global $CFG, $DB;
 187          $this->resetAfterTest();
 188  
 189          $CFG->showuseridentity = 'email,idnumber';
 190          $this->setAdminUser();
 191  
 192          list($sort, $params) = users_order_by_sql('u', 'search', \context_system::instance());
 193          $this->assert_same_sql('CASE WHEN
 194                      ' . $DB->sql_fullname('u.firstname', 'u.lastname') . ' = :usersortexact1 OR
 195                      LOWER(u.firstname) = LOWER(:usersortexact2) OR
 196                      LOWER(u.lastname) = LOWER(:usersortexact3) OR
 197                      LOWER(u.email) = LOWER(:usersortexact4) OR
 198                      LOWER(u.idnumber) = LOWER(:usersortexact5)
 199                  THEN 0 ELSE 1 END, u.lastname, u.firstname, u.id', $sort);
 200          $this->assertEquals(array('usersortexact1' => 'search', 'usersortexact2' => 'search',
 201                  'usersortexact3' => 'search', 'usersortexact4' => 'search', 'usersortexact5' => 'search'), $params);
 202      }
 203  
 204      public function test_users_order_by_sql_search_with_custom_fields(): void {
 205          global $CFG, $DB;
 206          $this->resetAfterTest();
 207  
 208          $CFG->showuseridentity = 'email,idnumber';
 209          $this->setAdminUser();
 210  
 211          list($sort, $params) =
 212                  users_order_by_sql('u', 'search', \context_system::instance(), ['profile_field_customfield' => 'x.customfield']);
 213          $this->assert_same_sql('CASE WHEN
 214                      ' . $DB->sql_fullname('u.firstname', 'u.lastname') . ' = :usersortexact1 OR
 215                      LOWER(u.firstname) = LOWER(:usersortexact2) OR
 216                      LOWER(u.lastname) = LOWER(:usersortexact3) OR
 217                      LOWER(x.customfield) = LOWER(:usersortexact4)
 218                  THEN 0 ELSE 1 END, u.lastname, u.firstname, u.id', $sort);
 219          $this->assertEquals(array('usersortexact1' => 'search', 'usersortexact2' => 'search',
 220                  'usersortexact3' => 'search', 'usersortexact4' => 'search'), $params);
 221      }
 222  
 223      public function test_get_admin() {
 224          global $CFG, $DB;
 225          $this->resetAfterTest();
 226  
 227          $this->assertSame('2', $CFG->siteadmins); // Admin always has id 2 in new installs.
 228          $defaultadmin = get_admin();
 229          $this->assertEquals($defaultadmin->id, 2);
 230  
 231          unset_config('siteadmins');
 232          $this->assertFalse(get_admin());
 233  
 234          set_config('siteadmins', -1);
 235          $this->assertFalse(get_admin());
 236  
 237          $user1 = $this->getDataGenerator()->create_user();
 238          $user2 = $this->getDataGenerator()->create_user();
 239  
 240          set_config('siteadmins', $user1->id.','.$user2->id);
 241          $admin = get_admin();
 242          $this->assertEquals($user1->id, $admin->id);
 243  
 244          set_config('siteadmins', '-1,'.$user2->id.','.$user1->id);
 245          $admin = get_admin();
 246          $this->assertEquals($user2->id, $admin->id);
 247  
 248          $odlread = $DB->perf_get_reads();
 249          get_admin(); // No DB queries on repeated call expected.
 250          get_admin();
 251          get_admin();
 252          $this->assertEquals($odlread, $DB->perf_get_reads());
 253      }
 254  
 255      public function test_get_admins() {
 256          global $CFG, $DB;
 257          $this->resetAfterTest();
 258  
 259          $this->assertSame('2', $CFG->siteadmins); // Admin always has id 2 in new installs.
 260  
 261          $user1 = $this->getDataGenerator()->create_user();
 262          $user2 = $this->getDataGenerator()->create_user();
 263          $user3 = $this->getDataGenerator()->create_user();
 264          $user4 = $this->getDataGenerator()->create_user();
 265  
 266          $admins = get_admins();
 267          $this->assertCount(1, $admins);
 268          $admin = reset($admins);
 269          $this->assertTrue(isset($admins[$admin->id]));
 270          $this->assertEquals(2, $admin->id);
 271  
 272          unset_config('siteadmins');
 273          $this->assertSame(array(), get_admins());
 274  
 275          set_config('siteadmins', -1);
 276          $this->assertSame(array(), get_admins());
 277  
 278          set_config('siteadmins', '-1,'.$user2->id.','.$user1->id.','.$user3->id);
 279          $this->assertEquals(array($user2->id=>$user2, $user1->id=>$user1, $user3->id=>$user3), get_admins());
 280  
 281          $odlread = $DB->perf_get_reads();
 282          get_admins(); // This should make just one query.
 283          $this->assertEquals($odlread+1, $DB->perf_get_reads());
 284      }
 285  
 286      public function test_get_course() {
 287          global $DB, $PAGE, $SITE;
 288          $this->resetAfterTest();
 289  
 290          // First test course will be current course ($COURSE).
 291          $course1obj = $this->getDataGenerator()->create_course(array('shortname' => 'FROGS'));
 292          $PAGE->set_course($course1obj);
 293  
 294          // Second test course is not current course.
 295          $course2obj = $this->getDataGenerator()->create_course(array('shortname' => 'ZOMBIES'));
 296  
 297          // Check it does not make any queries when requesting the $COURSE/$SITE.
 298          $before = $DB->perf_get_queries();
 299          $result = get_course($course1obj->id);
 300          $this->assertEquals($before, $DB->perf_get_queries());
 301          $this->assertSame('FROGS', $result->shortname);
 302          $result = get_course($SITE->id);
 303          $this->assertEquals($before, $DB->perf_get_queries());
 304  
 305          // Check it makes 1 query to request other courses.
 306          $result = get_course($course2obj->id);
 307          $this->assertSame('ZOMBIES', $result->shortname);
 308          $this->assertEquals($before + 1, $DB->perf_get_queries());
 309      }
 310  
 311      /**
 312       * Test that specifying fields when calling get_courses always returns required fields "id, category, visible"
 313       */
 314      public function test_get_courses_with_fields(): void {
 315          $this->resetAfterTest();
 316  
 317          $category = $this->getDataGenerator()->create_category();
 318          $course = $this->getDataGenerator()->create_course(['category' => $category->id]);
 319  
 320          // Specify "id" only.
 321          $courses = get_courses($category->id, 'c.sortorder', 'c.id');
 322          $this->assertCount(1, $courses);
 323          $this->assertEquals((object) [
 324              'id' => $course->id,
 325              'category' => $course->category,
 326              'visible' => $course->visible,
 327          ], reset($courses));
 328  
 329          // Specify some optional fields.
 330          $courses = get_courses($category->id, 'c.sortorder', 'c.id, c.shortname, c.fullname');
 331          $this->assertCount(1, $courses);
 332          $this->assertEquals((object) [
 333              'id' => $course->id,
 334              'category' => $course->category,
 335              'visible' => $course->visible,
 336              'shortname' => $course->shortname,
 337              'fullname' => $course->fullname,
 338          ], reset($courses));
 339      }
 340  
 341      public function test_increment_revision_number() {
 342          global $DB;
 343          $this->resetAfterTest();
 344  
 345          // Use one of the fields that are used with increment_revision_number().
 346          $course1 = $this->getDataGenerator()->create_course();
 347          $course2 = $this->getDataGenerator()->create_course();
 348          $DB->set_field('course', 'cacherev', 1, array());
 349  
 350          $record1 = $DB->get_record('course', array('id'=>$course1->id));
 351          $record2 = $DB->get_record('course', array('id'=>$course2->id));
 352          $this->assertEquals(1, $record1->cacherev);
 353          $this->assertEquals(1, $record2->cacherev);
 354  
 355          // Incrementing some lower value.
 356          $this->setCurrentTimeStart();
 357          increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id));
 358          $record1 = $DB->get_record('course', array('id'=>$course1->id));
 359          $record2 = $DB->get_record('course', array('id'=>$course2->id));
 360          $this->assertTimeCurrent($record1->cacherev);
 361          $this->assertEquals(1, $record2->cacherev);
 362  
 363          // Incrementing in the same second.
 364          $rev1 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id));
 365          $now = time();
 366          $DB->set_field('course', 'cacherev', $now, array('id'=>$course1->id));
 367          increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id));
 368          $rev2 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id));
 369          $this->assertGreaterThan($rev1, $rev2);
 370          increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id));
 371          $rev3 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id));
 372          $this->assertGreaterThan($rev2, $rev3);
 373          $this->assertGreaterThan($now+1, $rev3);
 374          increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id));
 375          $rev4 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id));
 376          $this->assertGreaterThan($rev3, $rev4);
 377          $this->assertGreaterThan($now+2, $rev4);
 378  
 379          // Recovering from runaway revision.
 380          $DB->set_field('course', 'cacherev', time()+60*60*60, array('id'=>$course2->id));
 381          $record2 = $DB->get_record('course', array('id'=>$course2->id));
 382          $this->assertGreaterThan(time(), $record2->cacherev);
 383          $this->setCurrentTimeStart();
 384          increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course2->id));
 385          $record2b = $DB->get_record('course', array('id'=>$course2->id));
 386          $this->assertTimeCurrent($record2b->cacherev);
 387  
 388          // Update all revisions.
 389          $DB->set_field('course', 'cacherev', 1, array());
 390          $this->setCurrentTimeStart();
 391          increment_revision_number('course', 'cacherev', '');
 392          $record1 = $DB->get_record('course', array('id'=>$course1->id));
 393          $record2 = $DB->get_record('course', array('id'=>$course2->id));
 394          $this->assertTimeCurrent($record1->cacherev);
 395          $this->assertEquals($record1->cacherev, $record2->cacherev);
 396      }
 397  
 398      public function test_get_coursemodule_from_id() {
 399          global $CFG;
 400  
 401          $this->resetAfterTest();
 402          $this->setAdminUser(); // Some generators have bogus access control.
 403  
 404          $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
 405          $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
 406  
 407          $course1 = $this->getDataGenerator()->create_course();
 408          $course2 = $this->getDataGenerator()->create_course();
 409  
 410          $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
 411          $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
 412          $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
 413  
 414          $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
 415  
 416          $cm = get_coursemodule_from_id('folder', $folder1a->cmid);
 417          $this->assertInstanceOf('stdClass', $cm);
 418          $this->assertSame('folder', $cm->modname);
 419          $this->assertSame($folder1a->id, $cm->instance);
 420          $this->assertSame($folder1a->course, $cm->course);
 421          $this->assertObjectNotHasAttribute('sectionnum', $cm);
 422  
 423          $this->assertEquals($cm, get_coursemodule_from_id('', $folder1a->cmid));
 424          $this->assertEquals($cm, get_coursemodule_from_id('folder', $folder1a->cmid, $course1->id));
 425          $this->assertEquals($cm, get_coursemodule_from_id('folder', $folder1a->cmid, 0));
 426          $this->assertFalse(get_coursemodule_from_id('folder', $folder1a->cmid, -10));
 427  
 428          $cm2 = get_coursemodule_from_id('folder', $folder1a->cmid, 0, true);
 429          $this->assertEquals(3, $cm2->sectionnum);
 430          unset($cm2->sectionnum);
 431          $this->assertEquals($cm, $cm2);
 432  
 433          $this->assertFalse(get_coursemodule_from_id('folder', -11));
 434  
 435          try {
 436              get_coursemodule_from_id('folder', -11, 0, false, MUST_EXIST);
 437              $this->fail('dml_missing_record_exception expected');
 438          } catch (\moodle_exception $e) {
 439              $this->assertInstanceOf('dml_missing_record_exception', $e);
 440          }
 441  
 442          try {
 443              get_coursemodule_from_id('', -11, 0, false, MUST_EXIST);
 444              $this->fail('dml_missing_record_exception expected');
 445          } catch (\moodle_exception $e) {
 446              $this->assertInstanceOf('dml_missing_record_exception', $e);
 447          }
 448  
 449          try {
 450              get_coursemodule_from_id('a b', $folder1a->cmid, 0, false, MUST_EXIST);
 451              $this->fail('coding_exception expected');
 452          } catch (\moodle_exception $e) {
 453              $this->assertInstanceOf('coding_exception', $e);
 454          }
 455  
 456          try {
 457              get_coursemodule_from_id('abc', $folder1a->cmid, 0, false, MUST_EXIST);
 458              $this->fail('dml_read_exception expected');
 459          } catch (\moodle_exception $e) {
 460              $this->assertInstanceOf('dml_read_exception', $e);
 461          }
 462      }
 463  
 464      public function test_get_coursemodule_from_instance() {
 465          global $CFG;
 466  
 467          $this->resetAfterTest();
 468          $this->setAdminUser(); // Some generators have bogus access control.
 469  
 470          $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
 471          $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
 472  
 473          $course1 = $this->getDataGenerator()->create_course();
 474          $course2 = $this->getDataGenerator()->create_course();
 475  
 476          $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
 477          $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
 478  
 479          $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
 480  
 481          $cm = get_coursemodule_from_instance('folder', $folder1a->id);
 482          $this->assertInstanceOf('stdClass', $cm);
 483          $this->assertSame('folder', $cm->modname);
 484          $this->assertSame($folder1a->id, $cm->instance);
 485          $this->assertSame($folder1a->course, $cm->course);
 486          $this->assertObjectNotHasAttribute('sectionnum', $cm);
 487  
 488          $this->assertEquals($cm, get_coursemodule_from_instance('folder', $folder1a->id, $course1->id));
 489          $this->assertEquals($cm, get_coursemodule_from_instance('folder', $folder1a->id, 0));
 490          $this->assertFalse(get_coursemodule_from_instance('folder', $folder1a->id, -10));
 491  
 492          $cm2 = get_coursemodule_from_instance('folder', $folder1a->id, 0, true);
 493          $this->assertEquals(3, $cm2->sectionnum);
 494          unset($cm2->sectionnum);
 495          $this->assertEquals($cm, $cm2);
 496  
 497          $this->assertFalse(get_coursemodule_from_instance('folder', -11));
 498  
 499          try {
 500              get_coursemodule_from_instance('folder', -11, 0, false, MUST_EXIST);
 501              $this->fail('dml_missing_record_exception expected');
 502          } catch (\moodle_exception $e) {
 503              $this->assertInstanceOf('dml_missing_record_exception', $e);
 504          }
 505  
 506          try {
 507              get_coursemodule_from_instance('a b', $folder1a->cmid, 0, false, MUST_EXIST);
 508              $this->fail('coding_exception expected');
 509          } catch (\moodle_exception $e) {
 510              $this->assertInstanceOf('coding_exception', $e);
 511          }
 512  
 513          try {
 514              get_coursemodule_from_instance('', $folder1a->cmid, 0, false, MUST_EXIST);
 515              $this->fail('coding_exception expected');
 516          } catch (\moodle_exception $e) {
 517              $this->assertInstanceOf('coding_exception', $e);
 518          }
 519  
 520          try {
 521              get_coursemodule_from_instance('abc', $folder1a->cmid, 0, false, MUST_EXIST);
 522              $this->fail('dml_read_exception expected');
 523          } catch (\moodle_exception $e) {
 524              $this->assertInstanceOf('dml_read_exception', $e);
 525          }
 526      }
 527  
 528      public function test_get_coursemodules_in_course() {
 529          global $CFG;
 530  
 531          $this->resetAfterTest();
 532          $this->setAdminUser(); // Some generators have bogus access control.
 533  
 534          $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
 535          $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
 536          $this->assertFileExists("$CFG->dirroot/mod/label/lib.php");
 537  
 538          $course1 = $this->getDataGenerator()->create_course();
 539          $course2 = $this->getDataGenerator()->create_course();
 540  
 541          $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
 542          $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
 543          $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
 544  
 545          $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
 546          $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
 547          $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
 548  
 549          $modules = get_coursemodules_in_course('folder', $course1->id);
 550          $this->assertCount(2, $modules);
 551  
 552          $cm = $modules[$folder1a->cmid];
 553          $this->assertSame('folder', $cm->modname);
 554          $this->assertSame($folder1a->id, $cm->instance);
 555          $this->assertSame($folder1a->course, $cm->course);
 556          $this->assertObjectNotHasAttribute('sectionnum', $cm);
 557          $this->assertObjectNotHasAttribute('revision', $cm);
 558          $this->assertObjectNotHasAttribute('display', $cm);
 559  
 560          $cm = $modules[$folder1b->cmid];
 561          $this->assertSame('folder', $cm->modname);
 562          $this->assertSame($folder1b->id, $cm->instance);
 563          $this->assertSame($folder1b->course, $cm->course);
 564          $this->assertObjectNotHasAttribute('sectionnum', $cm);
 565          $this->assertObjectNotHasAttribute('revision', $cm);
 566          $this->assertObjectNotHasAttribute('display', $cm);
 567  
 568          $modules = get_coursemodules_in_course('folder', $course1->id, 'revision, display');
 569          $this->assertCount(2, $modules);
 570  
 571          $cm = $modules[$folder1a->cmid];
 572          $this->assertSame('folder', $cm->modname);
 573          $this->assertSame($folder1a->id, $cm->instance);
 574          $this->assertSame($folder1a->course, $cm->course);
 575          $this->assertObjectNotHasAttribute('sectionnum', $cm);
 576          $this->assertObjectHasAttribute('revision', $cm);
 577          $this->assertObjectHasAttribute('display', $cm);
 578  
 579          $modules = get_coursemodules_in_course('label', $course1->id);
 580          $this->assertCount(0, $modules);
 581  
 582          try {
 583              get_coursemodules_in_course('a b', $course1->id);
 584              $this->fail('coding_exception expected');
 585          } catch (\moodle_exception $e) {
 586              $this->assertInstanceOf('coding_exception', $e);
 587          }
 588  
 589          try {
 590              get_coursemodules_in_course('abc', $course1->id);
 591              $this->fail('dml_read_exception expected');
 592          } catch (\moodle_exception $e) {
 593              $this->assertInstanceOf('dml_read_exception', $e);
 594          }
 595      }
 596  
 597      public function test_get_all_instances_in_courses() {
 598          global $CFG;
 599  
 600          $this->resetAfterTest();
 601          $this->setAdminUser(); // Some generators have bogus access control.
 602  
 603          $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
 604          $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
 605  
 606          $course1 = $this->getDataGenerator()->create_course();
 607          $course2 = $this->getDataGenerator()->create_course();
 608          $course3 = $this->getDataGenerator()->create_course();
 609  
 610          $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
 611          $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
 612          $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
 613  
 614          $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
 615          $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
 616          $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
 617  
 618          $folder3 = $this->getDataGenerator()->create_module('folder', array('course' => $course3));
 619  
 620          $modules = get_all_instances_in_courses('folder', array($course1->id => $course1, $course2->id => $course2));
 621          $this->assertCount(3, $modules);
 622  
 623          foreach ($modules as $cm) {
 624              if ($folder1a->cmid == $cm->coursemodule) {
 625                  $folder = $folder1a;
 626              } else if ($folder1b->cmid == $cm->coursemodule) {
 627                  $folder = $folder1b;
 628              } else if ($folder2->cmid == $cm->coursemodule) {
 629                  $folder = $folder2;
 630              } else {
 631                  $this->fail('Unexpected cm'. $cm->coursemodule);
 632              }
 633              $this->assertSame($folder->name, $cm->name);
 634              $this->assertSame($folder->course, $cm->course);
 635          }
 636  
 637          try {
 638              get_all_instances_in_courses('a b', array($course1->id => $course1, $course2->id => $course2));
 639              $this->fail('coding_exception expected');
 640          } catch (\moodle_exception $e) {
 641              $this->assertInstanceOf('coding_exception', $e);
 642          }
 643  
 644          try {
 645              get_all_instances_in_courses('', array($course1->id => $course1, $course2->id => $course2));
 646              $this->fail('coding_exception expected');
 647          } catch (\moodle_exception $e) {
 648              $this->assertInstanceOf('coding_exception', $e);
 649          }
 650      }
 651  
 652      public function test_get_all_instances_in_course() {
 653          global $CFG;
 654  
 655          $this->resetAfterTest();
 656          $this->setAdminUser(); // Some generators have bogus access control.
 657  
 658          $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php");
 659          $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php");
 660  
 661          $course1 = $this->getDataGenerator()->create_course();
 662          $course2 = $this->getDataGenerator()->create_course();
 663          $course3 = $this->getDataGenerator()->create_course();
 664  
 665          $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3));
 666          $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1));
 667          $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1));
 668  
 669          $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2));
 670          $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
 671          $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2));
 672  
 673          $folder3 = $this->getDataGenerator()->create_module('folder', array('course' => $course3));
 674  
 675          $modules = get_all_instances_in_course('folder', $course1);
 676          $this->assertCount(2, $modules);
 677  
 678          foreach ($modules as $cm) {
 679              if ($folder1a->cmid == $cm->coursemodule) {
 680                  $folder = $folder1a;
 681              } else if ($folder1b->cmid == $cm->coursemodule) {
 682                  $folder = $folder1b;
 683              } else {
 684                  $this->fail('Unexpected cm'. $cm->coursemodule);
 685              }
 686              $this->assertSame($folder->name, $cm->name);
 687              $this->assertSame($folder->course, $cm->course);
 688          }
 689  
 690          try {
 691              get_all_instances_in_course('a b', $course1);
 692              $this->fail('coding_exception expected');
 693          } catch (\moodle_exception $e) {
 694              $this->assertInstanceOf('coding_exception', $e);
 695          }
 696  
 697          try {
 698              get_all_instances_in_course('', $course1);
 699              $this->fail('coding_exception expected');
 700          } catch (\moodle_exception $e) {
 701              $this->assertInstanceOf('coding_exception', $e);
 702          }
 703      }
 704  
 705      /**
 706       * Test max courses in category
 707       */
 708      public function test_max_courses_in_category() {
 709          global $CFG;
 710          $this->resetAfterTest();
 711  
 712          // Default settings.
 713          $this->assertEquals(MAX_COURSES_IN_CATEGORY, get_max_courses_in_category());
 714  
 715          // Misc category.
 716          $misc = \core_course_category::get_default();
 717          $this->assertEquals(MAX_COURSES_IN_CATEGORY, $misc->sortorder);
 718  
 719          $category1 = $this->getDataGenerator()->create_category();
 720          $category2 = $this->getDataGenerator()->create_category();
 721  
 722          // Check category sort orders.
 723          $this->assertEquals(MAX_COURSES_IN_CATEGORY, \core_course_category::get($misc->id)->sortorder);
 724          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2, \core_course_category::get($category1->id)->sortorder);
 725          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3, \core_course_category::get($category2->id)->sortorder);
 726  
 727          // Create courses.
 728          $course1 = $this->getDataGenerator()->create_course(['category' => $category1->id]);
 729          $course2 = $this->getDataGenerator()->create_course(['category' => $category2->id]);
 730          $course3 = $this->getDataGenerator()->create_course(['category' => $category1->id]);
 731          $course4 = $this->getDataGenerator()->create_course(['category' => $category2->id]);
 732  
 733          // Check course sort orders.
 734          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 2, get_course($course1->id)->sortorder);
 735          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 2, get_course($course2->id)->sortorder);
 736          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 1, get_course($course3->id)->sortorder);
 737          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 1, get_course($course4->id)->sortorder);
 738  
 739          // Increase max course in category.
 740          $CFG->maxcoursesincategory = 20000;
 741          $this->assertEquals(20000, get_max_courses_in_category());
 742  
 743          // The sort order has not yet fixed, these sort orders should be the same as before.
 744          // Categories.
 745          $this->assertEquals(MAX_COURSES_IN_CATEGORY, \core_course_category::get($misc->id)->sortorder);
 746          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2, \core_course_category::get($category1->id)->sortorder);
 747          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3, \core_course_category::get($category2->id)->sortorder);
 748          // Courses in category 1.
 749          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 2, get_course($course1->id)->sortorder);
 750          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 1, get_course($course3->id)->sortorder);
 751          // Courses in category 2.
 752          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 2, get_course($course2->id)->sortorder);
 753          $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 1, get_course($course4->id)->sortorder);
 754  
 755          // Create new category so that the sort orders are applied.
 756          $category3 = $this->getDataGenerator()->create_category();
 757          // Categories.
 758          $this->assertEquals(20000, \core_course_category::get($misc->id)->sortorder);
 759          $this->assertEquals(20000 * 2, \core_course_category::get($category1->id)->sortorder);
 760          $this->assertEquals(20000 * 3, \core_course_category::get($category2->id)->sortorder);
 761          $this->assertEquals(20000 * 4, \core_course_category::get($category3->id)->sortorder);
 762          // Courses in category 1.
 763          $this->assertEquals(20000 * 2 + 2, get_course($course1->id)->sortorder);
 764          $this->assertEquals(20000 * 2 + 1, get_course($course3->id)->sortorder);
 765          // Courses in category 2.
 766          $this->assertEquals(20000 * 3 + 2, get_course($course2->id)->sortorder);
 767          $this->assertEquals(20000 * 3 + 1, get_course($course4->id)->sortorder);
 768      }
 769  
 770      /**
 771       * Test debug message for max courses in category
 772       */
 773      public function test_debug_max_courses_in_category() {
 774          global $CFG;
 775          $this->resetAfterTest();
 776  
 777          // Set to small value so that we can check the debug message.
 778          $CFG->maxcoursesincategory = 3;
 779          $this->assertEquals(3, get_max_courses_in_category());
 780  
 781          $category1 = $this->getDataGenerator()->create_category();
 782  
 783          // There is only one course, no debug message.
 784          $this->getDataGenerator()->create_course(['category' => $category1->id]);
 785          $this->assertDebuggingNotCalled();
 786          // There are two courses, no debug message.
 787          $this->getDataGenerator()->create_course(['category' => $category1->id]);
 788          $this->assertDebuggingNotCalled();
 789          // There is debug message when number of courses reaches the maximum number.
 790          $this->getDataGenerator()->create_course(['category' => $category1->id]);
 791          $this->assertDebuggingCalled("The number of courses (category id: $category1->id) has reached max number of courses " .
 792              "in a category (" . get_max_courses_in_category() . "). It will cause a sorting performance issue. " .
 793              "Please set higher value for \$CFG->maxcoursesincategory in config.php. " .
 794              "Please also make sure \$CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer. " .
 795              "See tracker issues: MDL-25669 and MDL-69573");
 796      }
 797  
 798      /**
 799       * Tests the get_users_listing function.
 800       */
 801      public function test_get_users_listing(): void {
 802          global $DB;
 803  
 804          $this->resetAfterTest();
 805  
 806          $generator = $this->getDataGenerator();
 807  
 808          // Set up profile field.
 809          $generator->create_custom_profile_field(['datatype' => 'text',
 810                  'shortname' => 'specialid', 'name' => 'Special user id']);
 811  
 812          // Set up the show user identity option.
 813          set_config('showuseridentity', 'department,profile_field_specialid');
 814  
 815          // Get all the existing user ids (we're going to remove these from test results).
 816          $existingids = array_fill_keys($DB->get_fieldset_select('user', 'id', '1 = 1'), true);
 817  
 818          // Create some test user accounts.
 819          $userids = [];
 820          foreach (['a', 'b', 'c', 'd'] as $key) {
 821              $record = [
 822                  'username' => 'user_' . $key,
 823                  'firstname' => $key . '_first',
 824                  'lastname' => 'last_' . $key,
 825                  'department' => 'department_' . $key,
 826                  'profile_field_specialid' => 'special_' . $key,
 827                  'lastaccess' => ord($key)
 828              ];
 829              $user = $generator->create_user($record);
 830              $userids[] = $user->id;
 831          }
 832  
 833          // Check default result with no parameters.
 834          $results = get_users_listing();
 835          $results = array_diff_key($results, $existingids);
 836  
 837          // It should return all the results in order.
 838          $this->assertEquals($userids, array_keys($results));
 839  
 840          // Results should have some general fields and name fields, check some samples.
 841          $this->assertEquals('user_a', $results[$userids[0]]->username);
 842          $this->assertEquals('user_a@example.com', $results[$userids[0]]->email);
 843          $this->assertEquals(1, $results[$userids[0]]->confirmed);
 844          $this->assertEquals('a_first', $results[$userids[0]]->firstname);
 845          $this->assertObjectHasAttribute('firstnamephonetic', $results[$userids[0]]);
 846  
 847          // Should not have the custom field or department because no context specified.
 848          $this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
 849          $this->assertObjectNotHasAttribute('profile_field_specialid', $results[$userids[0]]);
 850  
 851          // Check sorting.
 852          $results = get_users_listing('username', 'DESC');
 853          $results = array_diff_key($results, $existingids);
 854          $this->assertEquals([$userids[3], $userids[2], $userids[1], $userids[0]], array_keys($results));
 855  
 856          // Check default fallback sort field works as expected.
 857          $results = get_users_listing('blah2', 'ASC');
 858          $results = array_diff_key($results, $existingids);
 859          $this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
 860  
 861          // Check default fallback sort direction works as expected.
 862          $results = get_users_listing('lastaccess', 'blah2');
 863          $results = array_diff_key($results, $existingids);
 864          $this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
 865  
 866          // Add the options to showuseridentity and check it returns those fields but only if you
 867          // specify a context AND have permissions.
 868          $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
 869                  \context_system::instance());
 870          $this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
 871          $this->assertObjectNotHasAttribute('profile_field_specialid', $results[$userids[0]]);
 872          $this->setAdminUser();
 873          $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
 874                  \context_system::instance());
 875          $this->assertEquals('department_a', $results[$userids[0]]->department);
 876          $this->assertEquals('special_a', $results[$userids[0]]->profile_field_specialid);
 877  
 878          // Check search (full name, email, username).
 879          $results = get_users_listing('lastaccess', 'asc', 0, 0, 'b_first last_b');
 880          $this->assertEquals([$userids[1]], array_keys($results));
 881          $results = get_users_listing('lastaccess', 'asc', 0, 0, 'c@example');
 882          $this->assertEquals([$userids[2]], array_keys($results));
 883          $results = get_users_listing('lastaccess', 'asc', 0, 0, 'user_d');
 884          $this->assertEquals([$userids[3]], array_keys($results));
 885  
 886          // Check first and last initial restriction (all the test ones have same last initial).
 887          $results = get_users_listing('lastaccess', 'asc', 0, 0, '', 'C');
 888          $this->assertEquals([$userids[2]], array_keys($results));
 889          $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', 'L');
 890          $results = array_diff_key($results, $existingids);
 891          $this->assertEquals($userids, array_keys($results));
 892  
 893          // Check the extra where clause, either with the 'u.' prefix or not.
 894          $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
 895                  ['x' => $userids[1], 'y' => $userids[3]]);
 896          $results = array_diff_key($results, $existingids);
 897          $this->assertEquals([$userids[1], $userids[3]], array_keys($results));
 898          $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'u.id IN (:x,:y)',
 899                  ['x' => $userids[1], 'y' => $userids[3]]);
 900          $results = array_diff_key($results, $existingids);
 901          $this->assertEquals([$userids[1], $userids[3]], array_keys($results));
 902      }
 903  
 904      /**
 905       * Data provider for test_get_safe_orderby().
 906       *
 907       * @return array
 908       */
 909      public function get_safe_orderby_provider(): array {
 910          $orderbymap = [
 911              'courseid' => 'c.id',
 912              'somecustomvalue' => 'c.startdate, c.shortname',
 913              'default' => 'c.fullname',
 914          ];
 915          $orderbymapnodefault = [
 916              'courseid' => 'c.id',
 917              'somecustomvalue' => 'c.startdate, c.shortname',
 918          ];
 919  
 920          return [
 921              'Valid option, no direction specified' => [
 922                  $orderbymap,
 923                  'somecustomvalue',
 924                  '',
 925                  ' ORDER BY c.startdate, c.shortname',
 926              ],
 927              'Valid option, valid direction specified' => [
 928                  $orderbymap,
 929                  'courseid',
 930                  'DESC',
 931                  ' ORDER BY c.id DESC',
 932              ],
 933              'Valid option, valid lowercase direction specified' => [
 934                  $orderbymap,
 935                  'courseid',
 936                  'asc',
 937                  ' ORDER BY c.id ASC',
 938              ],
 939              'Valid option, invalid direction specified' => [
 940                  $orderbymap,
 941                  'courseid',
 942                  'BOOP',
 943                  ' ORDER BY c.id',
 944              ],
 945              'Valid option, invalid lowercase direction specified' => [
 946                  $orderbymap,
 947                  'courseid',
 948                  'boop',
 949                  ' ORDER BY c.id',
 950              ],
 951              'Invalid option default fallback, with valid direction' => [
 952                  $orderbymap,
 953                  'thisdoesnotexist',
 954                  'ASC',
 955                  ' ORDER BY c.fullname ASC',
 956              ],
 957              'Invalid option default fallback, with invalid direction' => [
 958                  $orderbymap,
 959                  'thisdoesnotexist',
 960                  'BOOP',
 961                  ' ORDER BY c.fullname',
 962              ],
 963              'Invalid option without default, with valid direction' => [
 964                  $orderbymapnodefault,
 965                  'thisdoesnotexist',
 966                  'ASC',
 967                  '',
 968              ],
 969              'Invalid option without default, with invalid direction' => [
 970                  $orderbymapnodefault,
 971                  'thisdoesnotexist',
 972                  'NOPE',
 973                  '',
 974              ],
 975          ];
 976      }
 977  
 978      /**
 979       * Tests the get_safe_orderby function.
 980       *
 981       * @dataProvider get_safe_orderby_provider
 982       * @param array $orderbymap The ORDER BY parameter mapping array.
 983       * @param string $orderbykey The string key being provided, to check against the map.
 984       * @param string $direction The optional direction to order by.
 985       * @param string $expected The expected string output of the method.
 986       */
 987      public function test_get_safe_orderby(array $orderbymap, string $orderbykey, string $direction, string $expected): void {
 988          $actual = get_safe_orderby($orderbymap, $orderbykey, $direction);
 989          $this->assertEquals($expected, $actual);
 990      }
 991  
 992      /**
 993       * Data provider for test_get_safe_orderby_multiple().
 994       *
 995       * @return array
 996       */
 997      public function get_safe_orderby_multiple_provider(): array {
 998          $orderbymap = [
 999              'courseid' => 'c.id',
1000              'firstname' => 'u.firstname',
1001              'default' => 'c.startdate',
1002          ];
1003          $orderbymapnodefault = [
1004              'courseid' => 'c.id',
1005              'firstname' => 'u.firstname',
1006          ];
1007  
1008          return [
1009              'Valid options, no directions specified' => [
1010                  $orderbymap,
1011                  ['courseid', 'firstname'],
1012                  [],
1013                  ' ORDER BY c.id, u.firstname',
1014              ],
1015              'Valid options, some direction specified' => [
1016                  $orderbymap,
1017                  ['courseid', 'firstname'],
1018                  ['DESC'],
1019                  ' ORDER BY c.id DESC, u.firstname',
1020              ],
1021              'Valid options, all directions specified' => [
1022                  $orderbymap,
1023                  ['courseid', 'firstname'],
1024                  ['ASC', 'desc'],
1025                  ' ORDER BY c.id ASC, u.firstname DESC',
1026              ],
1027              'Valid options, valid and invalid directions specified' => [
1028                  $orderbymap,
1029                  ['courseid', 'firstname'],
1030                  ['BOOP', 'DESC'],
1031                  ' ORDER BY c.id, u.firstname DESC',
1032              ],
1033              'Valid options, all invalid directions specified' => [
1034                  $orderbymap,
1035                  ['courseid', 'firstname'],
1036                  ['BOOP', 'SNOOT'],
1037                  ' ORDER BY c.id, u.firstname',
1038              ],
1039              'Valid and invalid option default fallback, with valid directions' => [
1040                  $orderbymap,
1041                  ['thisdoesnotexist', 'courseid'],
1042                  ['asc', 'DESC'],
1043                  ' ORDER BY c.startdate ASC, c.id DESC',
1044              ],
1045              'Valid and invalid option default fallback, with invalid direction' => [
1046                  $orderbymap,
1047                  ['courseid', 'thisdoesnotexist'],
1048                  ['BOOP', 'SNOOT'],
1049                  ' ORDER BY c.id, c.startdate',
1050              ],
1051              'Valid and invalid option without default, with valid direction' => [
1052                  $orderbymapnodefault,
1053                  ['thisdoesnotexist', 'courseid'],
1054                  ['ASC', 'DESC'],
1055                  ' ORDER BY c.id DESC',
1056              ],
1057              'Valid and invalid option without default, with invalid direction' => [
1058                  $orderbymapnodefault,
1059                  ['thisdoesnotexist', 'courseid'],
1060                  ['BOOP', 'SNOOT'],
1061                  ' ORDER BY c.id',
1062              ],
1063              'Invalid option only without default, with valid direction' => [
1064                  $orderbymapnodefault,
1065                  ['thisdoesnotexist'],
1066                  ['ASC'],
1067                  '',
1068              ],
1069              'Invalid option only without default, with invalid direction' => [
1070                  $orderbymapnodefault,
1071                  ['thisdoesnotexist'],
1072                  ['BOOP'],
1073                  '',
1074              ],
1075              'Single valid option, direction specified' => [
1076                  $orderbymap,
1077                  ['firstname'],
1078                  ['ASC'],
1079                  ' ORDER BY u.firstname ASC',
1080              ],
1081              'Single valid option, direction not specified' => [
1082                  $orderbymap,
1083                  ['firstname'],
1084                  [],
1085                  ' ORDER BY u.firstname',
1086              ],
1087          ];
1088      }
1089  
1090      /**
1091       * Tests the get_safe_orderby_multiple function.
1092       *
1093       * @dataProvider get_safe_orderby_multiple_provider
1094       * @param array $orderbymap The ORDER BY parameter mapping array.
1095       * @param array $orderbykeys The array of string keys being provided, to check against the map.
1096       * @param array $directions The optional directions to order by.
1097       * @param string $expected The expected string output of the method.
1098       */
1099      public function test_get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions,
1100              string $expected): void {
1101          $actual = get_safe_orderby_multiple($orderbymap, $orderbykeys, $directions);
1102          $this->assertEquals($expected, $actual);
1103      }
1104  }