Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  declare(strict_types=1);
  18  
  19  namespace core_user\reportbuilder\datasource;
  20  
  21  use core_collator;
  22  use core_reportbuilder_testcase;
  23  use core_reportbuilder_generator;
  24  use core_reportbuilder\local\filters\boolean_select;
  25  use core_reportbuilder\local\filters\date;
  26  use core_reportbuilder\local\filters\select;
  27  use core_reportbuilder\local\filters\tags;
  28  use core_reportbuilder\local\filters\text;
  29  use core_reportbuilder\local\filters\user as user_filter;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  global $CFG;
  34  require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php");
  35  
  36  /**
  37   * Unit tests for users datasource
  38   *
  39   * @package     core_user
  40   * @covers      \core_user\reportbuilder\datasource\users
  41   * @copyright   2022 Paul Holden <paulh@moodle.com>
  42   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class users_test extends core_reportbuilder_testcase {
  45  
  46      /**
  47       * Test default datasource
  48       */
  49      public function test_datasource_default(): void {
  50          $this->resetAfterTest();
  51  
  52          $user2 = $this->getDataGenerator()->create_user(['firstname' => 'Charles']);
  53          $user3 = $this->getDataGenerator()->create_user(['firstname' => 'Brian']);
  54  
  55          /** @var core_reportbuilder_generator $generator */
  56          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
  57          $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 1]);
  58  
  59          $content = $this->get_custom_report_content($report->get('id'));
  60          $this->assertCount(3, $content);
  61  
  62          // Default columns are fullname, username, email. Results are sorted by the fullname.
  63          [$adminrow, $userrow1, $userrow2] = array_map('array_values', $content);
  64  
  65          $this->assertEquals(['Admin User', 'admin', 'admin@example.com'], $adminrow);
  66          $this->assertEquals([fullname($user3), $user3->username, $user3->email], $userrow1);
  67          $this->assertEquals([fullname($user2), $user2->username, $user2->email], $userrow2);
  68      }
  69  
  70      /**
  71       * Test datasource columns that aren't added by default
  72       */
  73      public function test_datasource_non_default_columns(): void {
  74          $this->resetAfterTest();
  75  
  76          $user = $this->getDataGenerator()->create_user([
  77              'firstname' => 'Zoe',
  78              'idnumber' => 'U0001',
  79              'city' => 'London',
  80              'country' => 'GB',
  81              'interests' => ['Horses'],
  82          ]);
  83  
  84          /** @var core_reportbuilder_generator $generator */
  85          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
  86          $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]);
  87  
  88          // User.
  89          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithlink']);
  90          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicture']);
  91          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicturelink']);
  92          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:picture']);
  93          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']);
  94          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname']);
  95          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']);
  96          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']);
  97          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:description']);
  98          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstnamephonetic']);
  99          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastnamephonetic']);
 100          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:middlename']);
 101          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:alternatename']);
 102          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:idnumber']);
 103          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:institution']);
 104          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:department']);
 105          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:phone1']);
 106          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:phone2']);
 107          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:address']);
 108          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastaccess']);
 109          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended']);
 110          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:confirmed']);
 111          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:moodlenetprofile']);
 112          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:timecreated']);
 113  
 114          // Tags.
 115          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']);
 116          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:namewithlink']);
 117  
 118          $content = $this->get_custom_report_content($report->get('id'));
 119          $this->assertCount(2, $content);
 120  
 121          // Consistent order by firstname, just in case.
 122          core_collator::asort_array_of_arrays_by_key($content, 'c4_firstname');
 123          $content = array_values($content);
 124  
 125          [$adminrow, $userrow] = array_map('array_values', $content);
 126  
 127          $this->assertStringContainsString('Admin User', $adminrow[0]);
 128          $this->assertStringContainsString('Admin User', $adminrow[1]);
 129          $this->assertStringContainsString('Admin User', $adminrow[2]);
 130          $this->assertNotEmpty($adminrow[3]);
 131          $this->assertEquals('Admin', $adminrow[4]);
 132          $this->assertEquals('User', $adminrow[5]);
 133  
 134          $this->assertStringContainsString(fullname($user), $userrow[0]);
 135          $this->assertStringContainsString(fullname($user), $userrow[1]);
 136          $this->assertStringContainsString(fullname($user), $userrow[2]);
 137          $this->assertNotEmpty($userrow[3]);
 138          $this->assertEquals($user->firstname, $userrow[4]);
 139          $this->assertEquals($user->lastname, $userrow[5]);
 140          $this->assertEquals($user->city, $userrow[6]);
 141          $this->assertEquals('United Kingdom', $userrow[7]);
 142          $this->assertEquals($user->description, $userrow[8]);
 143          $this->assertEquals($user->firstnamephonetic, $userrow[9]);
 144          $this->assertEquals($user->lastnamephonetic, $userrow[10]);
 145          $this->assertEquals($user->middlename, $userrow[11]);
 146          $this->assertEquals($user->alternatename, $userrow[12]);
 147          $this->assertEquals($user->idnumber, $userrow[13]);
 148          $this->assertEquals($user->institution, $userrow[14]);
 149          $this->assertEquals($user->department, $userrow[15]);
 150          $this->assertEquals($user->phone1, $userrow[16]);
 151          $this->assertEquals($user->phone2, $userrow[17]);
 152          $this->assertEquals($user->address, $userrow[18]);
 153          $this->assertEmpty($userrow[19]);
 154          $this->assertEquals('No', $userrow[20]);
 155          $this->assertEquals('Yes', $userrow[21]);
 156          $this->assertEquals($user->moodlenetprofile, $userrow[22]);
 157          $this->assertNotEmpty($userrow[23]);
 158          $this->assertEquals('Horses', $userrow[24]);
 159          $this->assertStringContainsString('Horses', $userrow[25]);
 160      }
 161  
 162      /**
 163       * Data provider for {@see test_datasource_filters}
 164       *
 165       * @return array[]
 166       */
 167      public function datasource_filters_provider(): array {
 168          return [
 169              // User.
 170              'Filter user' => ['user:userselect', [
 171                  'user:userselect_operator' => user_filter::USER_SELECT,
 172                  'user:userselect_value' => [-1],
 173              ], false],
 174              'Filter fullname' => ['user:fullname', [
 175                  'user:fullname_operator' => text::CONTAINS,
 176                  'user:fullname_value' => 'Zoe',
 177              ], true],
 178              'Filter fullname (no match)' => ['user:fullname', [
 179                  'user:fullname_operator' => text::CONTAINS,
 180                  'user:fullname_value' => 'Alfie',
 181              ], false],
 182              'Filter firstname' => ['user:firstname', [
 183                  'user:firstname_operator' => text::IS_EQUAL_TO,
 184                  'user:firstname_value' => 'Zoe',
 185              ], true],
 186              'Filter firstname (no match)' => ['user:firstname', [
 187                  'user:firstname_operator' => text::IS_EQUAL_TO,
 188                  'user:firstname_value' => 'Alfie',
 189              ], false],
 190              'Filter middlename' => ['user:middlename', [
 191                  'user:middlename_operator' => text::IS_EQUAL_TO,
 192                  'user:middlename_value' => 'Zebediah',
 193              ], true],
 194              'Filter middlename (no match)' => ['user:middlename', [
 195                  'user:middlename_operator' => text::IS_EQUAL_TO,
 196                  'user:middlename_value' => 'Aardvark',
 197              ], false],
 198              'Filter lastname' => ['user:lastname', [
 199                  'user:lastname_operator' => text::IS_EQUAL_TO,
 200                  'user:lastname_value' => 'Zebra',
 201              ], true],
 202              'Filter lastname (no match)' => ['user:lastname', [
 203                  'user:lastname_operator' => text::IS_EQUAL_TO,
 204                  'user:lastname_value' => 'Aardvark',
 205              ], false],
 206              'Filter firstnamephonetic' => ['user:firstnamephonetic', [
 207                  'user:firstnamephonetic_operator' => text::IS_EQUAL_TO,
 208                  'user:firstnamephonetic_value' => 'Eoz',
 209              ], true],
 210              'Filter firstnamephonetic (no match)' => ['user:firstnamephonetic', [
 211                  'user:firstnamephonetic_operator' => text::IS_EQUAL_TO,
 212                  'user:firstnamephonetic_value' => 'Alfie',
 213              ], false],
 214              'Filter lastnamephonetic' => ['user:lastnamephonetic', [
 215                  'user:lastnamephonetic_operator' => text::IS_EQUAL_TO,
 216                  'user:lastnamephonetic_value' => 'Arbez',
 217              ], true],
 218              'Filter lastnamephonetic (no match)' => ['user:lastnamephonetic', [
 219                  'user:lastnamephonetic_operator' => text::IS_EQUAL_TO,
 220                  'user:lastnamephonetic_value' => 'Aardvark',
 221              ], false],
 222              'Filter alternatename' => ['user:alternatename', [
 223                  'user:alternatename_operator' => text::IS_EQUAL_TO,
 224                  'user:alternatename_value' => 'Zee',
 225              ], true],
 226              'Filter alternatename (no match)' => ['user:alternatename', [
 227                  'user:alternatename_operator' => text::IS_EQUAL_TO,
 228                  'user:alternatename_value' => 'Aardvark',
 229              ], false],
 230              'Filter email' => ['user:email', [
 231                  'user:email_operator' => text::CONTAINS,
 232                  'user:email_value' => 'zoe1',
 233              ], true],
 234              'Filter email (no match)' => ['user:email', [
 235                  'user:email_operator' => text::CONTAINS,
 236                  'user:email_value' => 'alfie1',
 237              ], false],
 238              'Filter phone1' => ['user:phone1', [
 239                  'user:phone1_operator' => text::IS_EQUAL_TO,
 240                  'user:phone1_value' => '111',
 241              ], true],
 242              'Filter phone1 (no match)' => ['user:phone1', [
 243                  'user:phone1_operator' => text::IS_EQUAL_TO,
 244                  'user:phone1_value' => '119',
 245              ], false],
 246              'Filter phone2' => ['user:phone2', [
 247                  'user:phone2_operator' => text::IS_EQUAL_TO,
 248                  'user:phone2_value' => '222',
 249              ], true],
 250              'Filter phone2 (no match)' => ['user:phone2', [
 251                  'user:phone2_operator' => text::IS_EQUAL_TO,
 252                  'user:phone2_value' => '229',
 253              ], false],
 254              'Filter address' => ['user:address', [
 255                  'user:address_operator' => text::IS_EQUAL_TO,
 256                  'user:address_value' => 'Big Farm',
 257              ], true],
 258              'Filter address (no match)' => ['user:address', [
 259                  'user:address_operator' => text::IS_EQUAL_TO,
 260                  'user:address_value' => 'Small Farm',
 261              ], false],
 262  
 263              'Filter city' => ['user:city', [
 264                  'user:city_operator' => text::IS_EQUAL_TO,
 265                  'user:city_value' => 'Barcelona',
 266              ], true],
 267              'Filter city (no match)' => ['user:city', [
 268                  'user:city_operator' => text::IS_EQUAL_TO,
 269                  'user:city_value' => 'Perth',
 270              ], false],
 271              'Filter country' => ['user:country', [
 272                  'user:country_operator' => select::EQUAL_TO,
 273                  'user:country_value' => 'ES',
 274              ], true],
 275              'Filter country (no match)' => ['user:country', [
 276                  'user:country_operator' => select::EQUAL_TO,
 277                  'user:country_value' => 'AU',
 278              ], false],
 279              'Filter description' => ['user:description', [
 280                  'user:description_operator' => text::CONTAINS,
 281                  'user:description_value' => 'Hello there',
 282              ], true],
 283              'Filter description (no match)' => ['user:description', [
 284                  'user:description_operator' => text::CONTAINS,
 285                  'user:description_value' => 'Goodbye',
 286              ], false],
 287              'Filter auth' => ['user:auth', [
 288                  'user:auth_operator' => select::EQUAL_TO,
 289                  'user:auth_value' => 'manual',
 290              ], true],
 291              'Filter auth (no match)' => ['user:auth', [
 292                  'user:auth_operator' => select::EQUAL_TO,
 293                  'user:auth_value' => 'ldap',
 294              ], false],
 295              'Filter username' => ['user:username', [
 296                  'user:username_operator' => text::IS_EQUAL_TO,
 297                  'user:username_value' => 'zoe1',
 298              ], true],
 299              'Filter username (no match)' => ['user:username', [
 300                  'user:username_operator' => text::IS_EQUAL_TO,
 301                  'user:username_value' => 'alfie1',
 302              ], false],
 303              'Filter idnumber' => ['user:idnumber', [
 304                  'user:idnumber_operator' => text::IS_EQUAL_TO,
 305                  'user:idnumber_value' => 'Z0001',
 306              ], true],
 307              'Filter idnumber (no match)' => ['user:idnumber', [
 308                  'user:idnumber_operator' => text::IS_EQUAL_TO,
 309                  'user:idnumber_value' => 'A0001',
 310              ], false],
 311              'Filter institution' => ['user:institution', [
 312                  'user:institution_operator' => text::IS_EQUAL_TO,
 313                  'user:institution_value' => 'Farm',
 314              ], true],
 315              'Filter institution (no match)' => ['user:institution', [
 316                  'user:institution_operator' => text::IS_EQUAL_TO,
 317                  'user:institution_value' => 'University',
 318              ], false],
 319              'Filter department' => ['user:department', [
 320                  'user:department_operator' => text::IS_EQUAL_TO,
 321                  'user:department_value' => 'Stable',
 322              ], true],
 323              'Filter department (no match)' => ['user:department', [
 324                  'user:department_operator' => text::IS_EQUAL_TO,
 325                  'user:department_value' => 'Office',
 326              ], false],
 327              'Filter moodlenetprofile' => ['user:moodlenetprofile', [
 328                  'user:moodlenetprofile_operator' => text::IS_EQUAL_TO,
 329                  'user:moodlenetprofile_value' => '@zoe1@example.com',
 330              ], true],
 331              'Filter moodlenetprofile (no match)' => ['user:moodlenetprofile', [
 332                  'user:moodlenetprofile_operator' => text::IS_EQUAL_TO,
 333                  'user:moodlenetprofile_value' => '@alfie1@example.com',
 334              ], false],
 335              'Filter suspended' => ['user:suspended', [
 336                  'user:suspended_operator' => boolean_select::NOT_CHECKED,
 337              ], true],
 338              'Filter suspended (no match)' => ['user:suspended', [
 339                  'user:suspended_operator' => boolean_select::CHECKED,
 340              ], false],
 341              'Filter confirmed' => ['user:confirmed', [
 342                  'user:confirmed_operator' => boolean_select::CHECKED,
 343              ], true],
 344              'Filter confirmed (no match)' => ['user:confirmed', [
 345                  'user:confirmed_operator' => boolean_select::NOT_CHECKED,
 346              ], false],
 347              'Filter timecreated' => ['user:timecreated', [
 348                  'user:timecreated_operator' => date::DATE_RANGE,
 349                  'user:timecreated_from' => 1622502000,
 350              ], true],
 351              'Filter timecreated (no match)' => ['user:timecreated', [
 352                  'user:timecreated_operator' => date::DATE_RANGE,
 353                  'user:timecreated_from' => 1619823600,
 354                  'user:timecreated_to' => 1622502000,
 355              ], false],
 356              'Filter lastaccess' => ['user:lastaccess', [
 357                  'user:lastaccess_operator' => date::DATE_EMPTY,
 358              ], true],
 359              'Filter lastaccess (no match)' => ['user:lastaccess', [
 360                  'user:lastaccess_operator' => date::DATE_RANGE,
 361                  'user:lastaccess_from' => 1619823600,
 362                  'user:lastaccess_to' => 1622502000,
 363              ], false],
 364  
 365              // Tags.
 366              'Filter tag name' => ['tag:name', [
 367                  'tag:name_operator' => tags::EQUAL_TO,
 368                  'tag:name_value' => [-1],
 369              ], false],
 370              'Filter tag name not empty' => ['tag:name', [
 371                  'tag:name_operator' => tags::NOT_EMPTY,
 372              ], true],
 373          ];
 374      }
 375  
 376      /**
 377       * Test datasource filters
 378       *
 379       * @param string $filtername
 380       * @param array $filtervalues
 381       * @param bool $expectmatch
 382       *
 383       * @dataProvider datasource_filters_provider
 384       */
 385      public function test_datasource_filters(string $filtername, array $filtervalues, bool $expectmatch): void {
 386          $this->resetAfterTest();
 387  
 388          $user = $this->getDataGenerator()->create_user([
 389              'username' => 'zoe1',
 390              'email' => 'zoe1@example.com',
 391              'firstname' => 'Zoe',
 392              'middlename' => 'Zebediah',
 393              'lastname' => 'Zebra',
 394              'firstnamephonetic' => 'Eoz',
 395              'lastnamephonetic' => 'Arbez',
 396              'alternatename' => 'Zee',
 397              'idnumber' => 'Z0001',
 398              'institution' => 'Farm',
 399              'department' => 'Stable',
 400              'phone1' => '111',
 401              'phone2' => '222',
 402              'address' => 'Big Farm',
 403              'city' => 'Barcelona',
 404              'country' => 'ES',
 405              'description' => 'Hello there',
 406              'moodlenetprofile' => '@zoe1@example.com',
 407              'interests' => ['Horses'],
 408          ]);
 409  
 410          /** @var core_reportbuilder_generator $generator */
 411          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
 412  
 413          // Create report containing single column, and given filter.
 414          $report = $generator->create_report(['name' => 'Tasks', 'source' => users::class, 'default' => 0]);
 415          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']);
 416  
 417          // Add filter, set it's values.
 418          $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]);
 419          $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues);
 420  
 421          if ($expectmatch) {
 422              $this->assertNotEmpty($content);
 423  
 424              // Merge report usernames into easily traversable array.
 425              $usernames = array_merge(...array_map('array_values', $content));
 426              $this->assertContains($user->username, $usernames);
 427          } else {
 428              $this->assertEmpty($content);
 429          }
 430      }
 431  
 432      /**
 433       * Stress test datasource
 434       *
 435       * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
 436       */
 437      public function test_stress_datasource(): void {
 438          if (!PHPUNIT_LONGTEST) {
 439              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 440          }
 441  
 442          $this->resetAfterTest();
 443  
 444          $user = $this->getDataGenerator()->create_user();
 445  
 446          $this->datasource_stress_test_columns(users::class);
 447          $this->datasource_stress_test_columns_aggregation(users::class);
 448          $this->datasource_stress_test_conditions(users::class, 'user:username');
 449      }
 450  }