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