Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Unit tests for core_table\local\filter\filterset.
  19   *
  20   * @package   core_table
  21   * @category  test
  22   * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
  24   */
  25  
  26  declare(strict_types=1);
  27  
  28  namespace core_table\local\filter;
  29  
  30  use InvalidArgumentException;
  31  use UnexpectedValueException;
  32  use advanced_testcase;
  33  use moodle_exception;
  34  
  35  /**
  36   * Unit tests for core_table\local\filter\filterset.
  37   *
  38   * @package   core_table
  39   * @category  test
  40   * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
  41   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class filterset_test extends advanced_testcase {
  44      /**
  45       * Ensure that it is possibly to set the join type.
  46       */
  47      public function test_set_join_type(): void {
  48          $filterset = $this->get_mocked_filterset();
  49  
  50          // Initial set with the default type should just work.
  51          // The setter should be chainable.
  52          $this->assertEquals($filterset, $filterset->set_join_type(filterset::JOINTYPE_DEFAULT));
  53          $this->assertEquals(filterset::JOINTYPE_DEFAULT, $filterset->get_join_type());
  54  
  55          // It should be possible to update the join type later.
  56          $this->assertEquals($filterset, $filterset->set_join_type(filterset::JOINTYPE_NONE));
  57          $this->assertEquals(filterset::JOINTYPE_NONE, $filterset->get_join_type());
  58  
  59          $this->assertEquals($filterset, $filterset->set_join_type(filterset::JOINTYPE_ANY));
  60          $this->assertEquals(filterset::JOINTYPE_ANY, $filterset->get_join_type());
  61  
  62          $this->assertEquals($filterset, $filterset->set_join_type(filterset::JOINTYPE_ALL));
  63          $this->assertEquals(filterset::JOINTYPE_ALL, $filterset->get_join_type());
  64      }
  65  
  66      /**
  67       * Ensure that it is not possible to provide a value out of bounds when setting the join type.
  68       */
  69      public function test_set_join_type_invalid_low(): void {
  70          $filterset = $this->get_mocked_filterset();
  71  
  72          // Valid join types are current 0, 1, or 2.
  73          // A value too low should be rejected.
  74          $this->expectException(InvalidArgumentException::class);
  75          $this->expectExceptionMessage("Invalid join type specified");
  76          $filterset->set_join_type(-1);
  77      }
  78  
  79      /**
  80       * Ensure that it is not possible to provide a value out of bounds when setting the join type.
  81       */
  82      public function test_set_join_type_invalid_high(): void {
  83          $filterset = $this->get_mocked_filterset();
  84  
  85          // Valid join types are current 0, 1, or 2.
  86          // A value too low should be rejected.
  87          $this->expectException(InvalidArgumentException::class);
  88          $this->expectExceptionMessage("Invalid join type specified");
  89          $filterset->set_join_type(4);
  90      }
  91  
  92      /**
  93       * Ensure that adding filter values works as expected.
  94       */
  95      public function test_add_filter_value(): void {
  96          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
  97          $filterset->method('get_optional_filters')
  98              ->will($this->returnValue([
  99                  'name' => filter::class,
 100                  'species' => filter::class,
 101              ]));
 102  
 103          // Initially an empty list.
 104          $this->assertEmpty($filterset->get_filters());
 105  
 106          // Test data.
 107          $speciesfilter = new filter('species', null, ['canine']);
 108          $namefilter = new filter('name', null, ['rosie']);
 109  
 110          // Add a filter to the list.
 111          $filterset->add_filter($speciesfilter);
 112          $this->assertSame([
 113              $speciesfilter,
 114          ], array_values($filterset->get_filters()));
 115  
 116          // Adding a second value should add that value.
 117          // The values should sorted.
 118          $filterset->add_filter($namefilter);
 119          $this->assertSame([
 120              $namefilter,
 121              $speciesfilter,
 122          ], array_values($filterset->get_filters()));
 123  
 124          // Adding an existing filter again should be ignored.
 125          $filterset->add_filter($speciesfilter);
 126          $this->assertSame([
 127              $namefilter,
 128              $speciesfilter,
 129          ], array_values($filterset->get_filters()));
 130      }
 131  
 132      /**
 133       * Ensure that it is possible to add a filter of a validated filter type.
 134       */
 135      public function test_add_filter_validated_type(): void {
 136          $namefilter = $this->getMockBuilder(filter::class)
 137              ->setConstructorArgs(['name'])
 138              ->setMethods(null)
 139              ->getMock();
 140          $namefilter->add_filter_value('rosie');
 141  
 142          // Mock the get_optional_filters function.
 143          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 144          $filterset->method('get_optional_filters')
 145              ->will($this->returnValue([
 146                   'name' => get_class($namefilter),
 147               ]));
 148  
 149          // Add a filter to the list.
 150          // This is the 'name' filter.
 151          $filterset->add_filter($namefilter);
 152  
 153          $this->assertNull($filterset->check_validity());
 154      }
 155  
 156      /**
 157       * Ensure that it is not possible to add a type which is not expected.
 158       */
 159      public function test_add_filter_unexpected_key(): void {
 160          // Mock the get_optional_filters function.
 161          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 162          $filterset->method('get_optional_filters')
 163              ->will($this->returnValue([]));
 164  
 165          // Add a filter to the list.
 166          // This is the 'name' filter.
 167          $namefilter = new filter('name');
 168  
 169          $this->expectException(InvalidArgumentException::class);
 170          $this->expectExceptionMessage("The filter 'name' was not recognised.");
 171          $filterset->add_filter($namefilter);
 172      }
 173  
 174      /**
 175       * Ensure that it is not possible to add a validated type where the type is incorrect.
 176       */
 177      public function test_add_filter_validated_type_incorrect(): void {
 178          $filtername = "name";
 179          $otherfilter = $this->createMock(filter::class);
 180  
 181          // Mock the get_optional_filters function.
 182          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 183          $filterset->method('get_optional_filters')
 184              ->will($this->returnValue([
 185                  $filtername => get_class($otherfilter),
 186              ]));
 187  
 188          // Add a filter to the list.
 189          // This is the 'name' filter.
 190          $namefilter = $this->getMockBuilder(filter::class)
 191              ->setMethods(null)
 192              ->setConstructorArgs([$filtername])
 193              ->getMock();
 194  
 195          $actualtype = get_class($namefilter);
 196          $requiredtype = get_class($otherfilter);
 197          $this->expectException(InvalidArgumentException::class);
 198          $this->expectExceptionMessage(
 199              "The filter '{$filtername}' was incorrectly specified as a {$actualtype}. It must be a {$requiredtype}."
 200          );
 201          $filterset->add_filter($namefilter);
 202      }
 203  
 204      /**
 205       * Ensure that a filter can be added from parameters provided to a web service.
 206       */
 207      public function test_add_filter_from_params(): void {
 208          $filtername = "name";
 209          $otherfilter = $this->getMockBuilder(filter::class)
 210              ->setMethods(null)
 211              ->setConstructorArgs([$filtername])
 212              ->getMock();
 213  
 214          // Mock the get_optional_filters function.
 215          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 216          $filterset->method('get_optional_filters')
 217              ->will($this->returnValue([
 218                  $filtername => get_class($otherfilter),
 219              ]));
 220  
 221          $result = $filterset->add_filter_from_params($filtername, filter::JOINTYPE_DEFAULT, ['kevin']);
 222  
 223          // The function is chainable.
 224          $this->assertEquals($filterset, $result);
 225  
 226          // Get the filter back.
 227          $filter = $filterset->get_filter($filtername);
 228          $this->assertEquals($filtername, $filter->get_name());
 229          $this->assertEquals(filter::JOINTYPE_DEFAULT, $filter->get_join_type());
 230          $this->assertEquals(['kevin'], $filter->get_filter_values());
 231      }
 232  
 233      /**
 234       * Ensure that an unknown filter is not added.
 235       */
 236      public function test_add_filter_from_params_unable_to_autoload(): void {
 237          // Mock the get_optional_filters function.
 238          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 239          $filterset->method('get_optional_filters')
 240              ->will($this->returnValue([
 241                  'name' => '\\moodle\\this\\is\\a\\fake\\class\\name',
 242              ]));
 243  
 244          $this->expectException(InvalidArgumentException::class);
 245          $this->expectExceptionMessage(
 246              "The filter class '\\moodle\\this\\is\\a\\fake\\class\\name' for filter 'name' could not be found."
 247          );
 248          $filterset->add_filter_from_params('name', filter::JOINTYPE_DEFAULT, ['kevin']);
 249      }
 250  
 251      /**
 252       * Ensure that an unknown filter is not added.
 253       */
 254      public function test_add_filter_from_params_invalid(): void {
 255          $filtername = "name";
 256          $otherfilter = $this->getMockBuilder(filter::class)
 257              ->setMethods(null)
 258              ->setConstructorArgs([$filtername])
 259              ->getMock();
 260  
 261          // Mock the get_optional_filters function.
 262          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 263          $filterset->method('get_optional_filters')
 264              ->will($this->returnValue([
 265                  $filtername => get_class($otherfilter),
 266              ]));
 267  
 268          $this->expectException(InvalidArgumentException::class);
 269          $this->expectExceptionMessage("The filter 'unknownfilter' was not recognised.");
 270          $filterset->add_filter_from_params('unknownfilter', filter::JOINTYPE_DEFAULT, ['kevin']);
 271      }
 272  
 273      /**
 274       * Ensure that adding a different filter with a different object throws an Exception.
 275       */
 276      public function test_duplicate_filter_value(): void {
 277          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 278          $filterset->method('get_optional_filters')
 279              ->will($this->returnValue([
 280                  'name' => filter::class,
 281                  'species' => filter::class,
 282              ]));
 283  
 284          // Add a filter to the list.
 285          // This is the 'name' filter.
 286          $namefilter = new filter('name', null, ['rosie']);
 287          $filterset->add_filter($namefilter);
 288  
 289          // Add another filter to the list.
 290          // This one has been incorrectly called the 'name' filter when it should be 'species'.
 291          $this->expectException(UnexpectedValueException::Class);
 292          $this->expectExceptionMessage("A filter of type 'name' has already been added. Check that you have the correct filter.");
 293  
 294          $speciesfilter = new filter('name', null, ['canine']);
 295          $filterset->add_filter($speciesfilter);
 296      }
 297  
 298      /**
 299       * Ensure that validating a filterset correctly compares filter types.
 300       */
 301      public function test_check_validity_optional_filters_not_specified(): void {
 302          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 303          $filterset->method('get_optional_filters')
 304              ->will($this->returnValue([
 305                  'name' => filter::class,
 306                  'species' => filter::class,
 307              ]));
 308  
 309          $this->assertNull($filterset->check_validity());
 310      }
 311  
 312      /**
 313       * Ensure that validating a filterset correctly requires required filters.
 314       */
 315      public function test_check_validity_required_filter(): void {
 316          $filterset = $this->get_mocked_filterset(['get_required_filters']);
 317          $filterset->expects($this->any())
 318              ->method('get_required_filters')
 319              ->willReturn([
 320                  'name' => filter::class
 321              ]);
 322  
 323          // Add a filter to the list.
 324          // This is the 'name' filter.
 325          $filterset->add_filter(new filter('name'));
 326  
 327          $this->assertNull($filterset->check_validity());
 328      }
 329  
 330      /**
 331       * Ensure that validating a filterset excepts correctly when a required fieldset is missing.
 332       */
 333      public function test_check_validity_filter_missing_required(): void {
 334          $filterset = $this->get_mocked_filterset(['get_required_filters']);
 335          $filterset->expects($this->any())
 336               ->method('get_required_filters')
 337               ->willReturn([
 338                   'name' => filter::class,
 339                   'species' => filter::class,
 340               ]);
 341  
 342          $this->expectException(moodle_exception::Class);
 343          $this->expectExceptionMessage("One or more required filters were missing (name, species)");
 344          $filterset->check_validity();
 345      }
 346  
 347      /**
 348       * Ensure that getting the filters returns a sorted list of filters.
 349       */
 350      public function test_get_filters(): void {
 351          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 352          $filterset->method('get_optional_filters')
 353              ->will($this->returnValue([
 354                  // Filters are not defined lexically.
 355                  'd' => filter::class,
 356                  'b' => filter::class,
 357                  'a' => filter::class,
 358                  'c' => filter::class,
 359              ]));
 360  
 361          // Filters are added in a different non-lexical order.
 362          $c = new filter('c');
 363          $filterset->add_filter($c);
 364  
 365          $b = new filter('b');
 366          $filterset->add_filter($b);
 367  
 368          $d = new filter('d');
 369          $filterset->add_filter($d);
 370  
 371          $a = new filter('a');
 372          $filterset->add_filter($a);
 373  
 374          // But they are returned lexically sorted.
 375          $this->assertEquals([
 376              'a' => $a,
 377              'b' => $b,
 378              'c' => $c,
 379              'd' => $d,
 380          ], $filterset->get_filters());
 381      }
 382  
 383      /**
 384       * Ensure that getting a singlel filter returns the correct filter.
 385       */
 386      public function test_get_filter(): void {
 387          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 388          $filterset->method('get_optional_filters')
 389              ->will($this->returnValue([
 390                  // Filters are not defined lexically.
 391                  'd' => filter::class,
 392                  'b' => filter::class,
 393                  'a' => filter::class,
 394                  'c' => filter::class,
 395              ]));
 396  
 397          // Filters are added in a different non-lexical order.
 398          $c = new filter('c');
 399          $filterset->add_filter($c);
 400  
 401          $b = new filter('b');
 402          $filterset->add_filter($b);
 403  
 404          $d = new filter('d');
 405          $filterset->add_filter($d);
 406  
 407          $a = new filter('a');
 408          $filterset->add_filter($a);
 409  
 410          // Filters can be individually retrieved in any order.
 411          $this->assertEquals($d, $filterset->get_filter('d'));
 412          $this->assertEquals($a, $filterset->get_filter('a'));
 413          $this->assertEquals($b, $filterset->get_filter('b'));
 414          $this->assertEquals($c, $filterset->get_filter('c'));
 415      }
 416  
 417      /**
 418       * Ensure that it is not possible to retrieve an unknown filter.
 419       */
 420      public function test_get_filter_unknown(): void {
 421          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 422          $filterset->method('get_optional_filters')
 423              ->will($this->returnValue([
 424                  'a' => filter::class,
 425              ]));
 426  
 427          $a = new filter('a');
 428          $filterset->add_filter($a);
 429  
 430          $this->expectException(UnexpectedValueException::Class);
 431          $this->expectExceptionMessage("The filter specified (d) is invalid.");
 432          $filterset->get_filter('d');
 433      }
 434  
 435      /**
 436       * Ensure that it is not possible to retrieve a valid filter before it is created.
 437       */
 438      public function test_get_filter_not_yet_added(): void {
 439          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 440          $filterset->method('get_optional_filters')
 441              ->will($this->returnValue([
 442                  'a' => filter::class,
 443              ]));
 444  
 445          $this->expectException(UnexpectedValueException::Class);
 446          $this->expectExceptionMessage("The filter specified (a) has not been created.");
 447          $filterset->get_filter('a');
 448      }
 449  
 450      /**
 451       * Ensure that the get_all_filtertypes function correctly returns the combined filterset.
 452       */
 453      public function test_get_all_filtertypes(): void {
 454          $otherfilter = $this->createMock(filter::class);
 455  
 456          $filterset = $this->get_mocked_filterset([
 457              'get_optional_filters',
 458              'get_required_filters',
 459          ]);
 460          $filterset->method('get_optional_filters')
 461              ->will($this->returnValue([
 462                  'a' => filter::class,
 463                  'c' => get_class($otherfilter),
 464              ]));
 465          $filterset->method('get_required_filters')
 466              ->will($this->returnValue([
 467                  'b' => get_class($otherfilter),
 468                  'd' => filter::class,
 469              ]));
 470  
 471          $this->assertEquals([
 472              'a' => filter::class,
 473              'b' => get_class($otherfilter),
 474              'c' => get_class($otherfilter),
 475              'd' => filter::class,
 476          ], $filterset->get_all_filtertypes());
 477      }
 478  
 479      /**
 480       * Ensure that the get_all_filtertypes function correctly returns the combined filterset.
 481       */
 482      public function test_get_all_filtertypes_conflict(): void {
 483          $otherfilter = $this->createMock(filter::class);
 484  
 485          $filterset = $this->get_mocked_filterset([
 486              'get_optional_filters',
 487              'get_required_filters',
 488          ]);
 489          $filterset->method('get_optional_filters')
 490              ->will($this->returnValue([
 491                  'a' => filter::class,
 492                  'b' => get_class($otherfilter),
 493                  'd' => filter::class,
 494              ]));
 495          $filterset->method('get_required_filters')
 496              ->will($this->returnValue([
 497                  'b' => get_class($otherfilter),
 498                  'c' => filter::class,
 499                  'd' => filter::class,
 500              ]));
 501  
 502          $this->expectException(InvalidArgumentException::Class);
 503          $this->expectExceptionMessage("Some filter types are both required, and optional: b, d");
 504          $filterset->get_all_filtertypes();
 505  
 506      }
 507  
 508      /**
 509       * Ensure that the has_filter function works as expected.
 510       */
 511      public function test_has_filter(): void {
 512          $filterset = $this->get_mocked_filterset(['get_optional_filters']);
 513          $filterset->method('get_optional_filters')
 514              ->will($this->returnValue([
 515                  // Define filters 'a', and 'b'.
 516                  'a' => filter::class,
 517                  'b' => filter::class,
 518              ]));
 519  
 520          // Only add filter 'a'.
 521          $a = new filter('a');
 522          $filterset->add_filter($a);
 523  
 524          // Filter 'a' should exist.
 525          $this->assertTrue($filterset->has_filter('a'));
 526  
 527          // Filter 'b' is defined, but has not been added.
 528          $this->assertFalse($filterset->has_filter('b'));
 529  
 530          // Filter 'c' is not defined.
 531          // No need to throw any kind of exception - this is an existence check.
 532          $this->assertFalse($filterset->has_filter('c'));
 533      }
 534  
 535      /**
 536       * Get a mocked copy of the filterset, mocking the specified methods.
 537       *
 538       * @param array $mockedmethods anonymous array containing the list of mocked methods
 539       * @return filterset Mock of the filterset
 540       */
 541      protected function get_mocked_filterset(array $mockedmethods = null): filterset {
 542          if (empty($mockedmethods)) {
 543              $mockedmethods = null;
 544          }
 545  
 546          return $this->getMockForAbstractClass(filterset::class, [], '', true, true, true, $mockedmethods);
 547      }
 548  }