Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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  2019 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 component_favourite_service within the service layer of favourites.
      30   *
      31   * @copyright  2019 Jake Dallimore <jrhdallimore@gmail.com>
      32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      33   */
      34  class component_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              ->onlyMethods([])
      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 the mockstore for all objects with properties matching the key => val pairs in $criteria.
      89                  foreach ($mockstore as $index => $mockrow) {
      90                      $mockrowarr = (array)$mockrow;
      91                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
      92                          $returns[$index] = $mockrow;
      93                      }
      94                  }
      95                  // Return a subset of the records, according to the paging options, if set.
      96                  if ($limitnum != 0) {
      97                      return array_slice($returns, $limitfrom, $limitnum);
      98                  }
      99                  // Otherwise, just return the full set.
     100                  return $returns;
     101              })
     102          );
     103          $mockrepo->expects($this->any())
     104              ->method('find_favourite')
     105              ->will($this->returnCallback(function(int $userid, string $comp, string $type, int $id, int $ctxid) use (&$mockstore) {
     106                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
     107                  $crit = ['userid' => $userid, 'component' => $comp, 'itemtype' => $type, 'itemid' => $id, 'contextid' => $ctxid];
     108                  foreach ($mockstore as $fakerow) {
     109                      $fakerowarr = (array)$fakerow;
     110                      if (array_diff_assoc($crit, $fakerowarr) == []) {
     111                          return $fakerow;
     112                      }
     113                  }
     114                  throw new \dml_missing_record_exception("Item not found");
     115              })
     116          );
     117          $mockrepo->expects($this->any())
     118              ->method('find')
     119              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
     120                  return $mockstore[$id];
     121              })
     122          );
     123          $mockrepo->expects($this->any())
     124              ->method('exists')
     125              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
     126                  return array_key_exists($id, $mockstore);
     127              })
     128          );
     129          $mockrepo->expects($this->any())
     130              ->method('count_by')
     131              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
     132                  $count = 0;
     133                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
     134                  foreach ($mockstore as $index => $mockrow) {
     135                      $mockrowarr = (array)$mockrow;
     136                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
     137                          $count++;
     138                      }
     139                  }
     140                  return $count;
     141              })
     142          );
     143          $mockrepo->expects($this->any())
     144              ->method('delete')
     145              ->will($this->returnCallback(function(int $id) use (&$mockstore) {
     146                  foreach ($mockstore as $mockrow) {
     147                      if ($mockrow->id == $id) {
     148                          unset($mockstore[$id]);
     149                      }
     150                  }
     151              })
     152          );
     153          $mockrepo->expects($this->any())
     154              ->method('delete_by')
     155              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
     156                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
     157                  foreach ($mockstore as $index => $mockrow) {
     158                      $mockrowarr = (array)$mockrow;
     159                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
     160                          unset($mockstore[$index]);
     161                      }
     162                  }
     163              })
     164          );
     165          $mockrepo->expects($this->any())
     166              ->method('exists_by')
     167              ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
     168                  // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
     169                  foreach ($mockstore as $index => $mockrow) {
     170                      $mockrowarr = (array)$mockrow;
     171                      echo "Here";
     172                      if (array_diff_assoc($criteria, $mockrowarr) == []) {
     173                          return true;
     174                      }
     175                  }
     176                  return false;
     177              })
     178          );
     179          return $mockrepo;
     180      }
     181  
     182      /**
     183       * Test confirming the deletion of favourites by type and item, but with no optional context filter provided.
     184       */
     185      public function test_delete_favourites_by_type_and_item() {
     186          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
     187  
     188          // Get a user_favourite_service for each user.
     189          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
     190          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
     191          $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
     192  
     193          // Favourite both courses for both users.
     194          $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
     195          $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
     196          $fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
     197          $fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
     198          $this->assertTrue($repo->exists($fav1->id));
     199          $this->assertTrue($repo->exists($fav2->id));
     200          $this->assertTrue($repo->exists($fav3->id));
     201          $this->assertTrue($repo->exists($fav4->id));
     202  
     203          // Favourite something else arbitrarily.
     204          $fav5 = $user2service->create_favourite('core_user', 'course', $course2context->instanceid, $course2context);
     205          $fav6 = $user2service->create_favourite('core_course', 'whatnow', $course2context->instanceid, $course2context);
     206  
     207          // Get a component_favourite_service to perform the type based deletion.
     208          $service = new \core_favourites\local\service\component_favourite_service('core_course', $repo);
     209  
     210          // Delete all 'course' type favourites (for all users who have favourited course1).
     211          $service->delete_favourites_by_type_and_item('course', $course1context->instanceid);
     212  
     213          // Delete all 'course' type favourites (for all users who have favourited course2).
     214          $service->delete_favourites_by_type_and_item('course', $course2context->instanceid);
     215  
     216          // Verify the favourites don't exist.
     217          $this->assertFalse($repo->exists($fav1->id));
     218          $this->assertFalse($repo->exists($fav2->id));
     219          $this->assertFalse($repo->exists($fav3->id));
     220          $this->assertFalse($repo->exists($fav4->id));
     221  
     222          // Verify favourites of other types or for other components are not affected.
     223          $this->assertTrue($repo->exists($fav5->id));
     224          $this->assertTrue($repo->exists($fav6->id));
     225  
     226          // Try to delete favourites for a type which we know doesn't exist. Verify no exception.
     227          $this->assertNull($service->delete_favourites_by_type_and_item('course', $course1context->instanceid));
     228      }
     229  
     230      /**
     231       * Test confirming the deletion of favourites by type and item and with the optional context filter provided.
     232       */
     233      public function test_delete_favourites_by_type_and_item_with_context() {
     234          list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
     235  
     236          // Get a user_favourite_service for each user.
     237          $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
     238          $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
     239          $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
     240  
     241          // Favourite both courses for both users.
     242          $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
     243          $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
     244          $fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
     245          $fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
     246          $this->assertTrue($repo->exists($fav1->id));
     247          $this->assertTrue($repo->exists($fav2->id));
     248          $this->assertTrue($repo->exists($fav3->id));
     249          $this->assertTrue($repo->exists($fav4->id));
     250  
     251          // Favourite something else arbitrarily.
     252          $fav5 = $user2service->create_favourite('core_user', 'course', $course1context->instanceid, $course1context);
     253          $fav6 = $user2service->create_favourite('core_course', 'whatnow', $course1context->instanceid, $course1context);
     254  
     255          // Favourite the courses again, but this time in another context.
     256          $fav7 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, context_system::instance());
     257          $fav8 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, context_system::instance());
     258          $fav9 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, context_system::instance());
     259          $fav10 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, context_system::instance());
     260  
     261          // Get a component_favourite_service to perform the type based deletion.
     262          $service = new \core_favourites\local\service\component_favourite_service('core_course', $repo);
     263  
     264          // Delete all 'course' type favourites (for all users at ONLY the course 1 context).
     265          $service->delete_favourites_by_type_and_item('course', $course1context->instanceid, $course1context);
     266  
     267          // Verify the favourites for course 1 context don't exist.
     268          $this->assertFalse($repo->exists($fav1->id));
     269          $this->assertFalse($repo->exists($fav2->id));
     270  
     271          // Verify the favourites for the same component and type, but NOT for the same contextid and unaffected.
     272          $this->assertTrue($repo->exists($fav3->id));
     273          $this->assertTrue($repo->exists($fav4->id));
     274  
     275          // Verify favourites of other types or for other components are not affected.
     276          $this->assertTrue($repo->exists($fav5->id));
     277          $this->assertTrue($repo->exists($fav6->id));
     278  
     279          // Verify the course favourite at the system context are unaffected.
     280          $this->assertTrue($repo->exists($fav7->id));
     281          $this->assertTrue($repo->exists($fav8->id));
     282          $this->assertTrue($repo->exists($fav9->id));
     283          $this->assertTrue($repo->exists($fav10->id));
     284  
     285          // Try to delete favourites for a type which we know doesn't exist. Verify no exception.
     286          $this->assertNull($service->delete_favourites_by_type_and_item('course', $course1context->instanceid, $course1context));
     287      }
     288  }