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 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  declare(strict_types=1);
  18  
  19  namespace core_files\reportbuilder\datasource;
  20  
  21  use core\context\{course, coursecat, user};
  22  use core_reportbuilder_generator;
  23  use core_reportbuilder_testcase;
  24  use core_reportbuilder\local\filters\{boolean_select, date, number, select, text};
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php");
  30  
  31  /**
  32   * Unit tests for files datasource
  33   *
  34   * @package     core_files
  35   * @covers      \core_files\reportbuilder\datasource\files
  36   * @copyright   2022 Paul Holden <paulh@moodle.com>
  37   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class files_test extends core_reportbuilder_testcase {
  40  
  41      /**
  42       * Test default datasource
  43       */
  44      public function test_datasource_default(): void {
  45          $this->resetAfterTest();
  46  
  47          $course = $this->getDataGenerator()->create_course();
  48          $coursecontext = course::instance($course->id);
  49  
  50          $user = $this->getDataGenerator()->create_user();
  51          $usercontext = user::instance($user->id);
  52  
  53          $this->setUser($user);
  54  
  55          $this->generate_test_files($coursecontext);
  56  
  57          /** @var core_reportbuilder_generator $generator */
  58          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
  59          $report = $generator->create_report(['name' => 'Files', 'source' => files::class, 'default' => 1]);
  60  
  61          $content = $this->get_custom_report_content($report->get('id'));
  62          $content = $this->filter_custom_report_content($content, static function(array $row): bool {
  63              return $row['c0_ctxid'] !== 'System';
  64          });
  65  
  66          $this->assertCount(2, $content);
  67  
  68          // Default columns are context, user, name, type, size, time created. Sorted by context and time created.
  69          [$contextname, $userfullname, $filename, $mimetype, $filesize, $timecreated] = array_values($content[0]);
  70          $this->assertEquals($coursecontext->get_context_name(), $contextname);
  71          $this->assertEquals(fullname($user), $userfullname);
  72          $this->assertEquals('Hello.txt', $filename);
  73          $this->assertEquals('Text file', $mimetype);
  74          $this->assertEquals("5\xc2\xa0bytes", $filesize);
  75          $this->assertNotEmpty($timecreated);
  76  
  77          [$contextname, $userfullname, $filename, $mimetype, $filesize, $timecreated] = array_values($content[1]);
  78          $this->assertEquals($usercontext->get_context_name(), $contextname);
  79          $this->assertEquals(fullname($user), $userfullname);
  80          $this->assertEquals('Hello.txt', $filename);
  81          $this->assertEquals('Text file', $mimetype);
  82          $this->assertEquals("5\xc2\xa0bytes", $filesize);
  83          $this->assertNotEmpty($timecreated);
  84      }
  85  
  86      /**
  87       * Test datasource columns that aren't added by default
  88       */
  89      public function test_datasource_non_default_columns(): void {
  90          $this->resetAfterTest();
  91          $this->setAdminUser();
  92  
  93          $category = $this->getDataGenerator()->create_category();
  94          $categorycontext = coursecat::instance($category->id);
  95  
  96          $course = $this->getDataGenerator()->create_course(['category' => $category->id]);
  97          $coursecontext = course::instance($course->id);
  98  
  99          $user = $this->getDataGenerator()->create_user();
 100          $usercontext = user::instance($user->id);
 101  
 102          $this->setUser($user);
 103  
 104          $draftitemid = $this->generate_test_files($coursecontext);
 105  
 106          /** @var core_reportbuilder_generator $generator */
 107          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
 108          $report = $generator->create_report(['name' => 'Files', 'source' => files::class, 'default' => 0]);
 109  
 110          // Consistent order, sorted by context and content hash.
 111          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:link',
 112              'sortenabled' => 1, 'sortorder' => 1]);
 113          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:name']);
 114          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:level']);
 115          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:path']);
 116          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:parent']);
 117  
 118          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:path']);
 119          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:author']);
 120          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:license']);
 121          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:contenthash',
 122              'sortenabled' => 1, 'sortorder' => 2]);
 123          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:component']);
 124          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:area']);
 125          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:itemid']);
 126  
 127          $content = $this->get_custom_report_content($report->get('id'));
 128          $content = $this->filter_custom_report_content($content, static function(array $row): bool {
 129              return stripos($row['c0_ctxid'], 'System') === false;
 130          });
 131  
 132          // There should be two entries (directory & file) for each context.
 133          $this->assertEquals([
 134              [
 135                  "<a href=\"{$coursecontext->get_url()}\">{$coursecontext->get_context_name()}</a>",
 136                  $coursecontext->get_context_name(),
 137                  'Course',
 138                  $coursecontext->path,
 139                  $categorycontext->get_context_name(),
 140                  '/',
 141                  null,
 142                  '',
 143                  'da39a3ee5e6b4b0d3255bfef95601890afd80709',
 144                  'course',
 145                  'summary',
 146                  0,
 147              ],
 148              [
 149                  "<a href=\"{$coursecontext->get_url()}\">{$coursecontext->get_context_name()}</a>",
 150                  $coursecontext->get_context_name(),
 151                  'Course',
 152                  $coursecontext->path,
 153                  $categorycontext->get_context_name(),
 154                  '/',
 155                  null,
 156                  '',
 157                  'f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0',
 158                  'course',
 159                  'summary',
 160                  0,
 161              ],
 162              [
 163                  "<a href=\"{$usercontext->get_url()}\">{$usercontext->get_context_name()}</a>",
 164                  $usercontext->get_context_name(),
 165                  'User',
 166                  $usercontext->path,
 167                  'System',
 168                  '/',
 169                  null,
 170                  '',
 171                  'da39a3ee5e6b4b0d3255bfef95601890afd80709',
 172                  'user',
 173                  'draft',
 174                  $draftitemid,
 175              ],
 176              [
 177                  "<a href=\"{$usercontext->get_url()}\">{$usercontext->get_context_name()}</a>",
 178                  $usercontext->get_context_name(),
 179                  'User',
 180                  $usercontext->path,
 181                  'System',
 182                  '/',
 183                  null,
 184                  '',
 185                  'f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0',
 186                  'user',
 187                  'draft',
 188                  $draftitemid,
 189              ],
 190          ], array_map('array_values', $content));
 191      }
 192  
 193      /**
 194       * Data provider for {@see test_datasource_filters}
 195       *
 196       * @return array[]
 197       */
 198      public function datasource_filters_provider(): array {
 199          return [
 200              // File.
 201              'Filter directory' => ['file:directory', [
 202                  'file:directory_operator' => boolean_select::CHECKED,
 203              ], 2],
 204              'Filter draft' => ['file:draft', [
 205                  'file:draft_operator' => boolean_select::CHECKED,
 206              ], 2],
 207              'Filter name' => ['file:name', [
 208                  'file:name_operator' => text::IS_EQUAL_TO,
 209                  'file:name_value' => 'Hello.txt',
 210              ], 2],
 211              'Filter size' => ['file:size', [
 212                  'file:size_operator' => number::GREATER_THAN,
 213                  'file:size_value1' => 2,
 214              ], 2],
 215              'Filter license' => ['file:license', [
 216                  'file:license_operator' => select::EQUAL_TO,
 217                  'file:license_value' => 'unknown',
 218              ], 4],
 219              'Filter license (non match)' => ['file:license', [
 220                  'file:license_operator' => select::EQUAL_TO,
 221                  'file:license_value' => 'public',
 222              ], 0],
 223              'Filter content hash' => ['file:contenthash', [
 224                  'file:contenthash_operator' => text::IS_EQUAL_TO,
 225                  'file:contenthash_value' => 'f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0',
 226              ], 2],
 227              'Filter content hash (no match)' => ['file:contenthash', [
 228                  'file:contenthash_operator' => text::IS_EQUAL_TO,
 229                  'file:contenthash_value' => 'f00f',
 230              ], 0],
 231              'Filter time created' => ['file:timecreated', [
 232                  'file:timecreated_operator' => date::DATE_RANGE,
 233                  'file:timecreated_from' => 1622502000,
 234              ], 4],
 235              'Filter time created (non match)' => ['file:timecreated', [
 236                  'file:timecreated_operator' => date::DATE_RANGE,
 237                  'file:timecreated_to' => 1622502000,
 238              ], 0],
 239  
 240              // Context.
 241              'Context level' => ['context:level', [
 242                  'context:level_operator' => select::EQUAL_TO,
 243                  'context:level_value' => CONTEXT_COURSE,
 244              ], 2],
 245              'Context level (no match)' => ['context:level', [
 246                  'context:level_operator' => select::EQUAL_TO,
 247                  'context:level_value' => CONTEXT_BLOCK,
 248              ], 0],
 249              'Context path' => ['context:path', [
 250                  'context:path_operator' => text::STARTS_WITH,
 251                  'context:path_value' => '/1/',
 252              ], 4],
 253              'Context path (no match)' => ['context:path', [
 254                  'context:path_operator' => text::STARTS_WITH,
 255                  'context:path_value' => '/1/2/3/',
 256              ], 0],
 257  
 258              // User.
 259              'Filter user' => ['user:username', [
 260                  'user:username_operator' => text::IS_EQUAL_TO,
 261                  'user:username_value' => 'alfie',
 262              ], 4],
 263              'Filter user (no match)' => ['user:username', [
 264                  'user:username_operator' => text::IS_EQUAL_TO,
 265                  'user:username_value' => 'lionel',
 266              ], 0],
 267          ];
 268      }
 269  
 270      /**
 271       * Test datasource filters
 272       *
 273       * @param string $filtername
 274       * @param array $filtervalues
 275       * @param int $expectmatchcount
 276       *
 277       * @dataProvider datasource_filters_provider
 278       */
 279      public function test_datasource_filters(
 280          string $filtername,
 281          array $filtervalues,
 282          int $expectmatchcount
 283      ): void {
 284          $this->resetAfterTest();
 285          $this->setAdminUser();
 286  
 287          $user = $this->getDataGenerator()->create_user(['username' => 'alfie']);
 288          $this->setUser($user);
 289  
 290          $course = $this->getDataGenerator()->create_course();
 291          $coursecontext = course::instance($course->id);
 292  
 293          $this->generate_test_files($coursecontext);
 294  
 295          /** @var core_reportbuilder_generator $generator */
 296          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
 297  
 298          // Create report containing single column, and given filter.
 299          $report = $generator->create_report(['name' => 'Files', 'source' => files::class, 'default' => 0]);
 300          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:name']);
 301  
 302          // Add filter, set it's values.
 303          $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]);
 304          $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues);
 305          $content = $this->filter_custom_report_content($content, static function(array $row): bool {
 306              return stripos($row['c0_ctxid'], 'System') === false;
 307          });
 308  
 309          $this->assertCount($expectmatchcount, $content);
 310      }
 311  
 312      /**
 313       * Stress test datasource
 314       *
 315       * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
 316       */
 317      public function test_stress_datasource(): void {
 318          if (!PHPUNIT_LONGTEST) {
 319              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 320          }
 321  
 322          $this->resetAfterTest();
 323          $this->setAdminUser();
 324  
 325          $course = $this->getDataGenerator()->create_course();
 326          $coursecontext = course::instance($course->id);
 327  
 328          $this->generate_test_files($coursecontext);
 329  
 330          $this->datasource_stress_test_columns(files::class);
 331          $this->datasource_stress_test_columns_aggregation(files::class);
 332          $this->datasource_stress_test_conditions(files::class, 'file:path');
 333      }
 334  
 335      /**
 336       * Ensuring report content only includes files we have explicitly created within the test
 337       *
 338       * @param array $content
 339       * @param callable $callback
 340       * @return array
 341       */
 342      protected function filter_custom_report_content(array $content, callable $callback): array {
 343          $content = array_filter($content, $callback);
 344          return array_values($content);
 345      }
 346  
 347      /**
 348       * Helper method to generate some test files (a user draft and course summary file) for reporting on
 349       *
 350       * @param course $context
 351       * @return int Draft item ID
 352       */
 353      protected function generate_test_files(course $context): int {
 354          global $USER;
 355  
 356          $draftitemid = file_get_unused_draft_itemid();
 357  
 358          // Populate user draft.
 359          get_file_storage()->create_file_from_string([
 360              'contextid' => user::instance($USER->id)->id,
 361              'userid' => $USER->id,
 362              'component' => 'user',
 363              'filearea' => 'draft',
 364              'itemid' => $draftitemid,
 365              'filepath' => '/',
 366              'filename' => 'Hello.txt',
 367          ], 'Hello');
 368  
 369          // Save draft to course summary file area.
 370          file_save_draft_area_files($draftitemid, $context->id, 'course', 'summary', 0);
 371  
 372          return $draftitemid;
 373      }
 374  }