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 310 and 401] [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core_favourites;
  18  
  19  use core_favourites\local\entity\favourite;
  20  
  21  /**
  22   * Test class covering the component_favourite_service within the service layer of favourites.
  23   *
  24   * @package    core_favourites
  25   * @category   test
  26   * @copyright  2019 Jake Dallimore <jrhdallimore@gmail.com>
  27   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  class component_favourite_service_test extends \advanced_testcase {
  30  
  31      public function setUp(): void {
  32          $this->resetAfterTest();
  33      }
  34  
  35      // Basic setup stuff to be reused in most tests.
  36      protected function setup_users_and_courses() {
  37          $user1 = self::getDataGenerator()->create_user();
  38          $user1context = \context_user::instance($user1->id);
  39          $user2 = self::getDataGenerator()->create_user();
  40          $user2context = \context_user::instance($user2->id);
  41          $course1 = self::getDataGenerator()->create_course();
  42          $course2 = self::getDataGenerator()->create_course();
  43          $course1context = \context_course::instance($course1->id);
  44          $course2context = \context_course::instance($course2->id);
  45          return [$user1context, $user2context, $course1context, $course2context];
  46      }
  47  
  48      /**
  49       * Generates an in-memory repository for testing, using an array store for CRUD stuff.
  50       *
  51       * @param array $mockstore
  52       * @return \PHPUnit\Framework\MockObject\MockObject
  53       */
  54      protected function get_mock_repository(array $mockstore) {
  55          // This mock will just store data in an array.
  56          $mockrepo = $this->getMockBuilder(\core_favourites\local\repository\favourite_repository_interface::class)
  57              ->onlyMethods([])
  58              ->getMock();
  59          $mockrepo->expects($this->any())
  60              ->method('add')
  61              ->will($this->returnCallback(function(favourite $favourite) use (&$mockstore) {
  62                  // Mock implementation of repository->add(), where an array is used instead of the DB.
  63                  // Duplicates are confirmed via the unique key, and exceptions thrown just like a real repo.
  64                  $key = $favourite->userid . $favourite->component . $favourite->itemtype . $favourite->itemid
  65                      . $favourite->contextid;
  66  
  67                  // Check the objects for the unique key.
  68                  foreach ($mockstore as $item) {
  69                      if ($item->uniquekey == $key) {
  70                          throw new \moodle_exception('Favourite already exists');
  71                      }
  72                  }
  73                  $index = count($mockstore);     // Integer index.
  74                  $favourite->uniquekey = $key;   // Simulate the unique key constraint.
  75                  $favourite->id = $index;
  76                  $mockstore[$index] = $favourite;
  77                  return $mockstore[$index];
  78              })
  79          );
  80          $mockrepo->expects($this->any())
  81              ->method('find_by')
  82              ->will($this->returnCallback(function(array $criteria, int $limitfrom = 0, int $limitnum = 0) use (&$mockstore) {
  83                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
  84                  foreach ($mockstore as $index => $mockrow) {
  85                      $mockrowarr = (array)$mockrow;
  86                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
  87                          $returns[$index] = $mockrow;
  88                      }
  89                  }
  90                  // Return a subset of the records, according to the paging options, if set.
  91                  if ($limitnum != 0) {
  92                      return array_slice($returns, $limitfrom, $limitnum);
  93                  }
  94                  // Otherwise, just return the full set.
  95                  return $returns;
  96              })
  97          );
  98          $mockrepo->expects($this->any())
  99              ->method('find_favourite')
 100              ->will($this->returnCallback(function(int $userid, string $comp, string $type, int $id, int $ctxid) use (&$mockstore) {
 101                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
 102                  $crit = ['userid' => $userid, 'component' => $comp, 'itemtype' => $type, 'itemid' => $id, 'contextid' => $ctxid];
 103                  foreach ($mockstore as $fakerow) {
 104                      $fakerowarr = (array)$fakerow;
 105                      if (array_diff_assoc($crit, $fakerowarr) == []) {
 106                          return $fakerow;
 107                      }
 108                  }
 109                  throw new \dml_missing_record_exception("Item not found");
 110              })
 111          );
 112          $mockrepo->expects($this->any())
 113              ->method('find')
 114              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
 115                  return $mockstore[$id];
 116              })
 117          );
 118          $mockrepo->expects($this->any())
 119              ->method('exists')
 120              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
 121                  return array_key_exists($id, $mockstore);
 122              })
 123          );
 124          $mockrepo->expects($this->any())
 125              ->method('count_by')
 126              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
 127                  $count = 0;
 128                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
 129                  foreach ($mockstore as $index => $mockrow) {
 130                      $mockrowarr = (array)$mockrow;
 131                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
 132                          $count++;
 133                      }
 134                  }
 135                  return $count;
 136              })
 137          );
 138          $mockrepo->expects($this->any())
 139              ->method('delete')
 140              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
 141                  foreach ($mockstore as $mockrow) {
 142                      if ($mockrow->id == $id) {
 143                          unset($mockstore[$id]);
 144                      }
 145                  }
 146              })
 147          );
 148          $mockrepo->expects($this->any())
 149              ->method('delete_by')
 150              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
 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                          unset($mockstore[$index]);
 156                      }
 157                  }
 158              })
 159          );
 160          $mockrepo->expects($this->any())
 161              ->method('exists_by')
 162              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
 163                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
 164                  foreach ($mockstore as $index => $mockrow) {
 165                      $mockrowarr = (array)$mockrow;
 166                      echo "Here";
 167                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
 168                          return true;
 169                      }
 170                  }
 171                  return false;
 172              })
 173          );
 174          return $mockrepo;
 175      }
 176  
 177      /**
 178       * Test confirming the deletion of favourites by type and item, but with no optional context filter provided.
 179       */
 180      public function test_delete_favourites_by_type_and_item() {
 181          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 182  
 183          // Get a user_favourite_service for each user.
 184          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
 185          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 186          $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
 187  
 188          // Favourite both courses for both users.
 189          $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 190          $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 191          $fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
 192          $fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
 193          $this->assertTrue($repo->exists($fav1->id));
 194          $this->assertTrue($repo->exists($fav2->id));
 195          $this->assertTrue($repo->exists($fav3->id));
 196          $this->assertTrue($repo->exists($fav4->id));
 197  
 198          // Favourite something else arbitrarily.
 199          $fav5 = $user2service->create_favourite('core_user', 'course', $course2context->instanceid, $course2context);
 200          $fav6 = $user2service->create_favourite('core_course', 'whatnow', $course2context->instanceid, $course2context);
 201  
 202          // Get a component_favourite_service to perform the type based deletion.
 203          $service = new \core_favourites\local\service\component_favourite_service('core_course', $repo);
 204  
 205          // Delete all 'course' type favourites (for all users who have favourited course1).
 206          $service->delete_favourites_by_type_and_item('course', $course1context->instanceid);
 207  
 208          // Delete all 'course' type favourites (for all users who have favourited course2).
 209          $service->delete_favourites_by_type_and_item('course', $course2context->instanceid);
 210  
 211          // Verify the favourites don't exist.
 212          $this->assertFalse($repo->exists($fav1->id));
 213          $this->assertFalse($repo->exists($fav2->id));
 214          $this->assertFalse($repo->exists($fav3->id));
 215          $this->assertFalse($repo->exists($fav4->id));
 216  
 217          // Verify favourites of other types or for other components are not affected.
 218          $this->assertTrue($repo->exists($fav5->id));
 219          $this->assertTrue($repo->exists($fav6->id));
 220  
 221          // Try to delete favourites for a type which we know doesn't exist. Verify no exception.
 222          $this->assertNull($service->delete_favourites_by_type_and_item('course', $course1context->instanceid));
 223      }
 224  
 225      /**
 226       * Test confirming the deletion of favourites by type and item and with the optional context filter provided.
 227       */
 228      public function test_delete_favourites_by_type_and_item_with_context() {
 229          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
 230  
 231          // Get a user_favourite_service for each user.
 232          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
 233          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
 234          $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
 235  
 236          // Favourite both courses for both users.
 237          $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 238          $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
 239          $fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
 240          $fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
 241          $this->assertTrue($repo->exists($fav1->id));
 242          $this->assertTrue($repo->exists($fav2->id));
 243          $this->assertTrue($repo->exists($fav3->id));
 244          $this->assertTrue($repo->exists($fav4->id));
 245  
 246          // Favourite something else arbitrarily.
 247          $fav5 = $user2service->create_favourite('core_user', 'course', $course1context->instanceid, $course1context);
 248          $fav6 = $user2service->create_favourite('core_course', 'whatnow', $course1context->instanceid, $course1context);
 249  
 250          // Favourite the courses again, but this time in another context.
 251          $fav7 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, \context_system::instance());
 252          $fav8 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, \context_system::instance());
 253          $fav9 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, \context_system::instance());
 254          $fav10 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, \context_system::instance());
 255  
 256          // Get a component_favourite_service to perform the type based deletion.
 257          $service = new \core_favourites\local\service\component_favourite_service('core_course', $repo);
 258  
 259          // Delete all 'course' type favourites (for all users at ONLY the course 1 context).
 260          $service->delete_favourites_by_type_and_item('course', $course1context->instanceid, $course1context);
 261  
 262          // Verify the favourites for course 1 context don't exist.
 263          $this->assertFalse($repo->exists($fav1->id));
 264          $this->assertFalse($repo->exists($fav2->id));
 265  
 266          // Verify the favourites for the same component and type, but NOT for the same contextid and unaffected.
 267          $this->assertTrue($repo->exists($fav3->id));
 268          $this->assertTrue($repo->exists($fav4->id));
 269  
 270          // Verify favourites of other types or for other components are not affected.
 271          $this->assertTrue($repo->exists($fav5->id));
 272          $this->assertTrue($repo->exists($fav6->id));
 273  
 274          // Verify the course favourite at the system context are unaffected.
 275          $this->assertTrue($repo->exists($fav7->id));
 276          $this->assertTrue($repo->exists($fav8->id));
 277          $this->assertTrue($repo->exists($fav9->id));
 278          $this->assertTrue($repo->exists($fav10->id));
 279  
 280          // Try to delete favourites for a type which we know doesn't exist. Verify no exception.
 281          $this->assertNull($service->delete_favourites_by_type_and_item('course', $course1context->instanceid, $course1context));
 282      }
 283  }