Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Testing the service layer within core_favourites.
  19   *
  20   * @package    core_favourites
  21   * @category   test
  22   * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  use \core_favourites\local\entity\favourite;
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Test class covering the user_favourite_service within the service layer of favourites.
  30   *
  31   * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class user_favourite_service_testcase extends advanced_testcase {
  35  
  36      public function setUp(): void {
  37          $this->resetAfterTest();
  38      }
  39  
  40      // Basic setup stuff to be reused in most tests.
  41      protected function setup_users_and_courses() {
  42          $user1 = self::getDataGenerator()->create_user();
  43          $user1context = \context_user::instance($user1->id);
  44          $user2 = self::getDataGenerator()->create_user();
  45          $user2context = \context_user::instance($user2->id);
  46          $course1 = self::getDataGenerator()->create_course();
  47          $course2 = self::getDataGenerator()->create_course();
  48          $course1context = context_course::instance($course1->id);
  49          $course2context = context_course::instance($course2->id);
  50          return [$user1context, $user2context, $course1context, $course2context];
  51      }
  52  
  53      /**
  54       * Generates an in-memory repository for testing, using an array store for CRUD stuff.
  55       *
  56       * @param array $mockstore
  57       * @return \PHPUnit\Framework\MockObject\MockObject
  58       */
  59      protected function get_mock_repository(array $mockstore) {
  60          // This mock will just store data in an array.
  61          $mockrepo = $this->getMockBuilder(\core_favourites\local\repository\favourite_repository_interface::class)
  62              ->setMethods([])
  63              ->getMock();
  64          $mockrepo->expects($this->any())
  65              ->method('add')
  66              ->will($this->returnCallback(function(favourite $favourite) use (&$mockstore) {
  67                  // Mock implementation of repository->add(), where an array is used instead of the DB.
  68                  // Duplicates are confirmed via the unique key, and exceptions thrown just like a real repo.
  69                  $key = $favourite->userid . $favourite->component . $favourite->itemtype . $favourite->itemid
  70                      . $favourite->contextid;
  71  
  72                  // Check the objects for the unique key.
  73                  foreach ($mockstore as $item) {
  74                      if ($item->uniquekey == $key) {
  75                          throw new \moodle_exception('Favourite already exists');
  76                      }
  77                  }
  78                  $index = count($mockstore);     // Integer index.
  79                  $favourite->uniquekey = $key;   // Simulate the unique key constraint.
  80                  $favourite->id = $index;
  81                  $mockstore[$index] = $favourite;
  82                  return $mockstore[$index];
  83              })
  84          );
  85          $mockrepo->expects($this->any())
  86              ->method('find_by')
  87              ->will($this->returnCallback(function(array $criteria, int $limitfrom = 0, int $limitnum = 0) use (&$mockstore) {
  88                  // Check for single value key pair vs multiple.
  89                  $multipleconditions = [];
  90                  foreach ($criteria as $key => $value) {
  91                      if (is_array($value)) {
  92                          $multipleconditions[$key] = $value;
  93                          unset($criteria[$key]);
  94                      }
  95                  }
  96  
  97                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
  98                  foreach ($mockstore as $index => $mockrow) {
  99                      $mockrowarr = (array)$mockrow;
 100                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
 101                          $found = true;
 102                          foreach ($multipleconditions as $key => $value) {
 103                              if (!in_array($mockrowarr[$key], $value)) {
 104                                  $found = false;
 105                                  break;
 106                              }
 107                          }
 108                          if ($found) {
 109                              $returns[$index] = $mockrow;
 110                          }
 111                      }
 112                  }
 113                  // Return a subset of the records, according to the paging options, if set.
 114                  if ($limitnum != 0) {
 115                      return array_slice($returns, $limitfrom, $limitnum);
 116                  }
 117                  // Otherwise, just return the full set.
 118                  return $returns;
 119              })
 120          );
 121          $mockrepo->expects($this->any())
 122              ->method('find_favourite')
 123              ->will($this->returnCallback(function(int $userid, string $comp, string $type, int $id, int $ctxid) use (&$mockstore) {
 124                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
 125                  $crit = ['userid' => $userid, 'component' => $comp, 'itemtype' => $type, 'itemid' => $id, 'contextid' => $ctxid];
 126                  foreach ($mockstore as $fakerow) {
 127                      $fakerowarr = (array)$fakerow;
 128                      if (array_diff_assoc($crit, $fakerowarr) == []) {
 129                          return $fakerow;
 130                      }
 131                  }
 132                  throw new \dml_missing_record_exception("Item not found");
 133              })
 134          );
 135          $mockrepo->expects($this->any())
 136              ->method('find')
 137              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
 138                  return $mockstore[$id];
 139              })
 140          );
 141          $mockrepo->expects($this->any())
 142              ->method('exists')
 143              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
 144                  return array_key_exists($id, $mockstore);
 145              })
 146          );
 147          $mockrepo->expects($this->any())
 148              ->method('count_by')
 149              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
 150                  $count = 0;
 151                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
 152                  foreach ($mockstore as $index => $mockrow) {
 153                      $mockrowarr = (array)$mockrow;
 154                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
 155                          $count++;
 156                      }
 157                  }
 158                  return $count;
 159              })
 160          );
 161          $mockrepo->expects($this->any())
 162              ->method('delete')
 163              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
 164                  foreach ($mockstore as $mockrow) {
 165                      if ($mockrow->id == $id) {
 166                          unset($mockstore[$id]);
 167                      }
 168                  }
 169              })
 170          );
 171          $mockrepo->expects($this->any())
 172              ->method('exists_by')
 173              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
 174                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
 175                  foreach ($mockstore as $index => $mockrow) {
 176                      $mockrowarr = (array)$mockrow;
 177                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
 178                          return true;
 179                      }
 180                  }
 181                  return false;
 182              })
 183          );
 184          return $mockrepo;
 185      }
 186  
 187      /**
 188       * Test getting a user_favourite_service from the static locator.
 189       */
 190      public function test_get_service_for_user_context() {
 191          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 192          $userservice = \core_favourites\service_factory::get_service_for_user_context($user1context);
 193          $this->assertInstanceOf(\core_favourites\local\service\user_favourite_service::class, $userservice);
 194      }
 195  
 196      /**
 197       * Test confirming an item can be favourited only once.
 198       */
 199      public function test_create_favourite_basic() {
 200          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 201  
 202          // Get a user_favourite_service for a user.
 203          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
 204          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 205  
 206          // Favourite a course.
 207          $favourite1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 208          $this->assertObjectHasAttribute('id', $favourite1);
 209  
 210          // Try to favourite the same course again.
 211          $this->expectException('moodle_exception');
 212          $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 213      }
 214  
 215      /**
 216       * Test confirming that an exception is thrown if trying to favourite an item for a non-existent component.
 217       */
 218      public function test_create_favourite_nonexistent_component() {
 219          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 220  
 221          // Get a user_favourite_service for the user.
 222          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
 223          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 224  
 225          // Try to favourite something in a non-existent component.
 226          $this->expectException('moodle_exception');
 227          $user1service->create_favourite('core_cccourse', 'my_area', $course1context->instanceid, $course1context);
 228      }
 229  
 230      /**
 231       * Test fetching favourites for single user, by area.
 232       */
 233      public function test_find_favourites_by_type_single_user() {
 234          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 235  
 236          // Get a user_favourite_service for the user.
 237          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
 238          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 239  
 240          // Favourite 2 courses, in separate areas.
 241          $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 242          $fav2 = $service->create_favourite('core_course', 'anothertype', $course2context->instanceid, $course2context);
 243  
 244          // Verify we can get favourites by area.
 245          $favourites = $service->find_favourites_by_type('core_course', 'course');
 246          $this->assertIsArray($favourites);
 247          $this->assertCount(1, $favourites); // We only get favourites for the 'core_course/course' area.
 248          $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
 249  
 250          $favourites = $service->find_favourites_by_type('core_course', 'anothertype');
 251          $this->assertIsArray($favourites);
 252          $this->assertCount(1, $favourites); // We only get favourites for the 'core_course/course' area.
 253          $this->assertEquals($fav2->id, $favourites[$fav2->id]->id);
 254      }
 255  
 256      /**
 257       * Test fetching favourites for single user, by area.
 258       */
 259      public function test_find_all_favourites() {
 260          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 261  
 262          // Get a user_favourite_service for the user.
 263          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
 264          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 265  
 266          // Favourite 2 courses, in separate areas.
 267          $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 268          $fav2 = $service->create_favourite('core_course', 'anothertype', $course2context->instanceid, $course2context);
 269          $fav3 = $service->create_favourite('core_course', 'yetanothertype', $course2context->instanceid, $course2context);
 270  
 271          // Verify we can get favourites by area.
 272          $favourites = $service->find_all_favourites('core_course', ['course']);
 273          $this->assertIsArray($favourites);
 274          $this->assertCount(1, $favourites); // We only get favourites for the 'core_course/course' area.
 275          $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
 276  
 277          $favourites = $service->find_all_favourites('core_course', ['course', 'anothertype']);
 278          $this->assertIsArray($favourites);
 279          // We only get favourites for the 'core_course/course' and 'core_course/anothertype area.
 280          $this->assertCount(2, $favourites);
 281          $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
 282          $this->assertEquals($fav2->id, $favourites[$fav2->id]->id);
 283  
 284          $favourites = $service->find_all_favourites('core_course');
 285          $this->assertIsArray($favourites);
 286          $this->assertCount(3, $favourites); // We only get favourites for the 'core_cours' area.
 287          $this->assertEquals($fav2->id, $favourites[$fav2->id]->id);
 288          $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
 289          $this->assertEquals($fav3->id, $favourites[$fav3->id]->id);
 290      }
 291  
 292      /**
 293       * Make sure the find_favourites_by_type() method only returns favourites for the scoped user.
 294       */
 295      public function test_find_favourites_by_type_multiple_users() {
 296          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 297  
 298          // Get a user_favourite_service for 2 users.
 299          $repo = $this->get_mock_repository([]);
 300          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 301          $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
 302  
 303          // Now, as each user, favourite the same course.
 304          $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 305          $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 306  
 307          // Verify find_favourites_by_type only returns results for the user to which the service is scoped.
 308          $user1favourites = $user1service->find_favourites_by_type('core_course', 'course');
 309          $this->assertIsArray($user1favourites);
 310          $this->assertCount(1, $user1favourites); // We only get favourites for the 'core_course/course' area for $user1.
 311          $this->assertEquals($fav1->id, $user1favourites[$fav1->id]->id);
 312  
 313          $user2favourites = $user2service->find_favourites_by_type('core_course', 'course');
 314          $this->assertIsArray($user2favourites);
 315          $this->assertCount(1, $user2favourites); // We only get favourites for the 'core_course/course' area for $user2.
 316          $this->assertEquals($fav2->id, $user2favourites[$fav2->id]->id);
 317      }
 318  
 319      /**
 320       * Test confirming that an exception is thrown if trying to get favourites for a non-existent component.
 321       */
 322      public function test_find_favourites_by_type_nonexistent_component() {
 323          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 324  
 325          // Get a user_favourite_service for the user.
 326          $repo = $this->get_mock_repository([]);
 327          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 328  
 329          // Verify we get an exception if we try to search for favourites in an invalid component.
 330          $this->expectException('moodle_exception');
 331          $service->find_favourites_by_type('cccore_notreal', 'something');
 332      }
 333  
 334      /**
 335       * Test confirming the pagination support for the find_favourites_by_type() method.
 336       */
 337      public function test_find_favourites_by_type_pagination() {
 338          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 339  
 340          // Get a user_favourite_service for the user.
 341          $repo = $this->get_mock_repository([]);
 342          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 343  
 344          // Favourite 10 arbitrary items.
 345          foreach (range(1, 10) as $i) {
 346              $service->create_favourite('core_course', 'course', $i, $course1context);
 347          }
 348  
 349          // Verify we have 10 favourites.
 350          $this->assertCount(10, $service->find_favourites_by_type('core_course', 'course'));
 351  
 352          // Verify we get back 5 favourites for page 1.
 353          $favourites = $service->find_favourites_by_type('core_course', 'course', 0, 5);
 354          $this->assertCount(5, $favourites);
 355  
 356          // Verify we get back 5 favourites for page 2.
 357          $favourites = $service->find_favourites_by_type('core_course', 'course', 5, 5);
 358          $this->assertCount(5, $favourites);
 359  
 360          // Verify we get back an empty array if querying page 3.
 361          $favourites = $service->find_favourites_by_type('core_course', 'course', 10, 5);
 362          $this->assertCount(0, $favourites);
 363      }
 364  
 365      /**
 366       * Test confirming the basic deletion behaviour.
 367       */
 368      public function test_delete_favourite_basic() {
 369          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 370  
 371          // Get a user_favourite_service for the user.
 372          $repo = $this->get_mock_repository([]);
 373          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 374  
 375          // Favourite a course.
 376          $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 377          $this->assertTrue($repo->exists($fav1->id));
 378  
 379          // Delete the favourite.
 380          $service->delete_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 381  
 382          // Verify the favourite doesn't exist.
 383          $this->assertFalse($repo->exists($fav1->id));
 384  
 385          // Try to delete a favourite which we know doesn't exist.
 386          $this->expectException(\moodle_exception::class);
 387          $service->delete_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 388      }
 389  
 390      /**
 391       * Test confirming the behaviour of the favourite_exists() method.
 392       */
 393      public function test_favourite_exists() {
 394          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 395  
 396          // Get a user_favourite_service for the user.
 397          $repo = $this->get_mock_repository([]);
 398          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 399  
 400          // Favourite a course.
 401          $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 402  
 403          // Verify we can check existence of the favourite.
 404          $this->assertTrue(
 405              $service->favourite_exists(
 406                  'core_course',
 407                  'course',
 408                  $course1context->instanceid,
 409                  $course1context
 410              )
 411          );
 412  
 413          // And one that we know doesn't exist.
 414          $this->assertFalse(
 415              $service->favourite_exists(
 416                  'core_course',
 417                  'someothertype',
 418                  $course1context->instanceid,
 419                  $course1context
 420              )
 421          );
 422      }
 423  
 424      /**
 425       * Test confirming the behaviour of the get_favourite() method.
 426       */
 427      public function test_get_favourite() {
 428          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 429  
 430          // Get a user_favourite_service for the user.
 431          $repo = $this->get_mock_repository([]);
 432          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 433  
 434          // Favourite a course.
 435          $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 436  
 437          $result = $service->get_favourite(
 438              'core_course',
 439              'course',
 440              $course1context->instanceid,
 441              $course1context
 442          );
 443          // Verify we can get the favourite.
 444          $this->assertEquals($fav1->id, $result->id);
 445  
 446          // And one that we know doesn't exist.
 447          $this->assertNull(
 448              $service->get_favourite(
 449                  'core_course',
 450                  'someothertype',
 451                  $course1context->instanceid,
 452                  $course1context
 453              )
 454          );
 455      }
 456  
 457      /**
 458       * Test confirming the behaviour of the count_favourites_by_type() method.
 459       */
 460      public function test_count_favourites_by_type() {
 461          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 462  
 463          // Get a user_favourite_service for the user.
 464          $repo = $this->get_mock_repository([]);
 465          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 466  
 467          $this->assertEquals(0, $service->count_favourites_by_type('core_course', 'course', $course1context));
 468          // Favourite a course.
 469          $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 470  
 471          $this->assertEquals(1, $service->count_favourites_by_type('core_course', 'course', $course1context));
 472  
 473          // Favourite another course.
 474          $service->create_favourite('core_course', 'course', $course2context->instanceid, $course1context);
 475  
 476          $this->assertEquals(2, $service->count_favourites_by_type('core_course', 'course', $course1context));
 477  
 478          // Favourite a course in another context.
 479          $service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
 480  
 481          // Doesn't affect original context.
 482          $this->assertEquals(2, $service->count_favourites_by_type('core_course', 'course', $course1context));
 483          // Gets counted if we include all contexts.
 484          $this->assertEquals(3, $service->count_favourites_by_type('core_course', 'course'));
 485      }
 486  
 487      /**
 488       * Verify that the join sql generated by get_join_sql_by_type is valid and can be used to include favourite information.
 489       */
 490      public function test_get_join_sql_by_type() {
 491          global $DB;
 492          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 493  
 494          // Get a user_favourite_service for the user.
 495          // We need to use a real (DB) repository, as we want to run the SQL.
 496          $repo = new \core_favourites\local\repository\favourite_repository();
 497          $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 498  
 499          // Favourite the first course only.
 500          $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 501  
 502          // Generate the join snippet.
 503          list($favsql, $favparams) = $service->get_join_sql_by_type('core_course', 'course', 'favalias', 'c.id');
 504  
 505          // Join against a simple select, including the 2 courses only.
 506          $params = ['courseid1' => $course1context->instanceid, 'courseid2' => $course2context->instanceid];
 507          $params = $params + $favparams;
 508          $records = $DB->get_records_sql("SELECT c.id, favalias.component
 509                                             FROM {course} c $favsql
 510                                            WHERE c.id = :courseid1 OR c.id = :courseid2", $params);
 511  
 512          // Verify the favourite information is returned, but only for the favourited course.
 513          $this->assertCount(2, $records);
 514          $this->assertEquals('core_course', $records[$course1context->instanceid]->component);
 515          $this->assertEmpty($records[$course2context->instanceid]->component);
 516      }
 517  }