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 enrol_lti\local\ltiadvantage\repository; 18 use enrol_lti\local\ltiadvantage\entity\application_registration; 19 use enrol_lti\local\ltiadvantage\entity\user; 20 21 /** 22 * Tests for user_repository objects. 23 * 24 * @package enrol_lti 25 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\user_repository 28 */ 29 class user_repository_test extends \advanced_testcase { 30 /** 31 * Helper to generate a new user instance. 32 * 33 * @param int $mockresourceid used to spoof a published resource, to which this user is associated. 34 * @param array $userfields user information like city, timezone which would normally come from the tool configuration. 35 * @return user a user instance 36 */ 37 protected function generate_user(int $mockresourceid = 1, array $userfields = []): user { 38 global $CFG; 39 $registration = application_registration::create( 40 'Test', 41 'a2c94a2c94', 42 new \moodle_url('http://lms.example.org'), 43 'clientid_123', 44 new \moodle_url('https://example.org/authrequesturl'), 45 new \moodle_url('https://example.org/jwksurl'), 46 new \moodle_url('https://example.org/accesstokenurl') 47 ); 48 $registrationrepo = new application_registration_repository(); 49 $createdregistration = $registrationrepo->save($registration); 50 51 $deployment = $createdregistration->add_tool_deployment('Deployment 1', 'DeployID123'); 52 $deploymentrepo = new deployment_repository(); 53 $saveddeployment = $deploymentrepo->save($deployment); 54 55 $contextrepo = new context_repository(); 56 $context = $saveddeployment->add_context( 57 'CTX123', 58 ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection'] 59 ); 60 $savedcontext = $contextrepo->save($context); 61 62 $resourcelinkrepo = new resource_link_repository(); 63 $resourcelink = $saveddeployment->add_resource_link('resourcelinkid_123', $mockresourceid, 64 $savedcontext->get_id()); 65 $savedresourcelink = $resourcelinkrepo->save($resourcelink); 66 67 // Create a user using the DB defaults to simulate what would have occurred during an auth_lti user auth. 68 $user = $this->getDataGenerator()->create_user([ 69 'city' => '', 70 'country' => '', 71 'institution' => '', 72 'timezone' => '99', 73 'maildisplay' => 2, 74 'lang' => 'en' 75 ]); 76 77 $userdefaultvalues = [ 78 'lang' => $CFG->lang, 79 'city' => '', 80 'country' => '', 81 'institution' => '', 82 'timezone' => '99', 83 'maildisplay' => 2 84 ]; 85 if (empty($userfields)) { 86 // If userfields is omitted, assume the tool default configuration values (as if 'User default values' are unchanged). 87 $userfields = $userdefaultvalues; 88 } else { 89 // If they have been provided, merge and override the defaults. 90 $userfields = array_merge($userdefaultvalues, $userfields); 91 } 92 $ltiuser = $savedresourcelink->add_user( 93 $user->id, 94 'source-id-123', 95 ...array_values($userfields) 96 ); 97 98 $ltiuser->set_lastgrade(67.33333333); 99 100 return $ltiuser; 101 } 102 103 /** 104 * Helper to assert that all the key elements of two users (i.e. excluding id) are equal. 105 * 106 * @param user $expected the user whose values are deemed correct. 107 * @param user $check the user to check. 108 * @param bool $checkresourcelink whether or not to confirm the resource link value matches too. 109 */ 110 protected function assert_same_user_values(user $expected, user $check, bool $checkresourcelink = false): void { 111 $this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid()); 112 $this->assertEquals($expected->get_city(), $check->get_city()); 113 $this->assertEquals($expected->get_country(), $check->get_country()); 114 $this->assertEquals($expected->get_institution(), $check->get_institution()); 115 $this->assertEquals($expected->get_timezone(), $check->get_timezone()); 116 $this->assertEquals($expected->get_maildisplay(), $check->get_maildisplay()); 117 $this->assertEquals($expected->get_lang(), $check->get_lang()); 118 if ($checkresourcelink) { 119 $this->assertEquals($expected->get_resourcelinkid(), $check->get_resourcelinkid()); 120 } 121 } 122 123 /** 124 * Helper to assert that all the key elements of a user are present in the DB. 125 * 126 * @param user $expected the user whose values are deemed correct. 127 */ 128 protected function assert_user_db_values(user $expected) { 129 global $DB; 130 $sql = "SELECT u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone, 131 u.maildisplay, u.mnethostid, u.confirmed, u.lang, u.auth 132 FROM {enrol_lti_users} lu 133 JOIN {user} u 134 ON (lu.userid = u.id) 135 WHERE lu.id = :id"; 136 $userrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]); 137 $this->assertEquals($expected->get_city(), $userrecord->city); 138 $this->assertEquals($expected->get_country(), $userrecord->country); 139 $this->assertEquals($expected->get_institution(), $userrecord->institution); 140 $this->assertEquals($expected->get_timezone(), $userrecord->timezone); 141 $this->assertEquals($expected->get_maildisplay(), $userrecord->maildisplay); 142 $this->assertEquals($expected->get_lang(), $userrecord->lang); 143 144 $ltiuserrecord = $DB->get_record('enrol_lti_users', ['id' => $expected->get_id()]); 145 $this->assertEquals($expected->get_id(), $ltiuserrecord->id); 146 $this->assertEquals($expected->get_sourceid(), $ltiuserrecord->sourceid); 147 $this->assertEquals($expected->get_resourceid(), $ltiuserrecord->toolid); 148 $this->assertEquals($expected->get_lastgrade(), $ltiuserrecord->lastgrade); 149 150 if ($expected->get_resourcelinkid()) { 151 $sql = "SELECT rl.id 152 FROM {enrol_lti_users} lu 153 JOIN {enrol_lti_user_resource_link} rlj 154 ON (lu.id = rlj.ltiuserid) 155 JOIN {enrol_lti_resource_link} rl 156 ON (rl.id = rlj.resourcelinkid) 157 WHERE lu.id = :id"; 158 $resourcelinkrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]); 159 $this->assertEquals($expected->get_resourcelinkid(), $resourcelinkrecord->id); 160 } 161 } 162 163 /** 164 * Tests adding a user to the store, assuming that the user has been created using the default 'user default values'. 165 * 166 * @covers ::save 167 */ 168 public function test_save_new_unchanged_user_defaults() { 169 $this->resetAfterTest(); 170 $user = $this->generate_user(); 171 $userrepo = new user_repository(); 172 $sink = $this->redirectEvents(); 173 $saveduser = $userrepo->save($user); 174 $events = $sink->get_events(); 175 $sink->close(); 176 177 $this->assertIsInt($saveduser->get_id()); 178 $this->assert_same_user_values($user, $saveduser, true); 179 $this->assert_user_db_values($saveduser); 180 // No change to underlying user: city, etc. take on default values matching those of the existing user record. 181 $this->assertEmpty($events); 182 } 183 184 /** 185 * Tests adding a user to the store, assuming that the user has been created using modified 'user default values'. 186 * 187 * @covers ::save 188 */ 189 public function test_save_new_changed_user_defaults() { 190 $this->resetAfterTest(); 191 $user = $this->generate_user(1, ['city' => 'Perth']); 192 $userrepo = new user_repository(); 193 $sink = $this->redirectEvents(); 194 $saveduser = $userrepo->save($user); 195 $events = $sink->get_events(); 196 $sink->close(); 197 198 $this->assertIsInt($saveduser->get_id()); 199 $this->assert_same_user_values($user, $saveduser, true); 200 $this->assert_user_db_values($saveduser); 201 // The underlying user record will change: city ('Perth') differs from that of the existing user (''). 202 $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); 203 } 204 205 /** 206 * Test saving an existing user instance. 207 * 208 * @covers ::save 209 */ 210 public function test_save_existing() { 211 $this->resetAfterTest(); 212 $user = $this->generate_user(); 213 $userrepo = new user_repository(); 214 $sink = $this->redirectEvents(); 215 $saveduser = $userrepo->save($user); 216 $events = $sink->get_events(); 217 $sink->close(); 218 $this->assertEmpty($events); // No event for the first save, since the underlying user record is unchanged. 219 220 $saveduser->set_city('New City'); 221 $saveduser->set_country('NZ'); 222 $saveduser->set_lastgrade(99.99999999); 223 $sink = $this->redirectEvents(); 224 $saveduser2 = $userrepo->save($saveduser); 225 $events = $sink->get_events(); 226 $sink->close(); 227 228 $this->assertEquals($saveduser->get_id(), $saveduser2->get_id()); 229 $this->assert_same_user_values($saveduser, $saveduser2, true); 230 $this->assert_user_db_values($saveduser2); 231 // The underlying user record will change now, since city and country have changed. 232 $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); 233 } 234 235 /** 236 * Test saving an instance which exists by id, but has a different localid to the data in the store. 237 * 238 * @covers ::save 239 */ 240 public function test_save_existing_localid_mismatch() { 241 $this->resetAfterTest(); 242 $user = $this->generate_user(); 243 $userrepo = new user_repository(); 244 $saveduser = $userrepo->save($user); 245 246 $user2 = user::create( 247 $saveduser->get_resourceid(), 248 999999, 249 $saveduser->get_deploymentid(), 250 $saveduser->get_sourceid(), 251 $saveduser->get_lang(), 252 $saveduser->get_timezone(), 253 '', 254 '', 255 '', 256 null, 257 null, 258 null, 259 null, 260 $saveduser->get_id() 261 ); 262 $this->expectException(\coding_exception::class); 263 $this->expectExceptionMessage("Cannot update user mapping. LTI user '{$saveduser->get_id()}' is already mapped " . 264 "to user '{$saveduser->get_localid()}' and can't be associated with another user '999999'."); 265 $userrepo->save($user2); 266 } 267 268 /** 269 * Test trying to save a user with an id that is invalid. 270 * 271 * @covers ::save 272 */ 273 public function test_save_stale_id() { 274 global $CFG; 275 $this->resetAfterTest(); 276 $instructoruser = $this->getDataGenerator()->create_user(); 277 $userrepo = new user_repository(); 278 $user = user::create( 279 4, 280 $instructoruser->id, 281 5, 282 'source-id-123', 283 $CFG->lang, 284 '99', 285 '', 286 '', 287 '', 288 null, 289 null, 290 null, 291 null, 292 999999 293 ); 294 295 $this->expectException(\coding_exception::class); 296 $this->expectExceptionMessage("Cannot save lti user with id '999999'. The record does not exist."); 297 $userrepo->save($user); 298 } 299 300 /** 301 * Verify that trying to save a stale object results in an exception referring to unique constraint violation. 302 * 303 * @covers ::save 304 */ 305 public function test_save_uniqueness_constraint() { 306 $this->resetAfterTest(); 307 $user = $this->generate_user(); 308 $userrepo = new user_repository(); 309 $userrepo->save($user); 310 311 $this->expectException(\coding_exception::class); 312 $this->expectExceptionMessageMatches("/Cannot create duplicate LTI user '[a-z0-9_]*' for resource '[0-9]*'/"); 313 $userrepo->save($user); 314 } 315 316 /** 317 * Test finding a user instance by id. 318 * 319 * @covers ::find 320 */ 321 public function test_find() { 322 $this->resetAfterTest(); 323 $user = $this->generate_user(); 324 $userrepo = new user_repository(); 325 $saveduser = $userrepo->save($user); 326 327 $founduser = $userrepo->find($saveduser->get_id()); 328 $this->assertIsInt($founduser->get_id()); 329 $this->assert_same_user_values($saveduser, $founduser, false); 330 331 $this->assertNull($userrepo->find(0)); 332 } 333 334 /** 335 * Test finding all of users associated with a given published resource. 336 * 337 * @covers ::find_by_resource 338 */ 339 public function test_find_by_resource() { 340 global $CFG; 341 $this->resetAfterTest(); 342 $user = $this->generate_user(); 343 $userrepo = new user_repository(); 344 $saveduser = $userrepo->save($user); 345 $instructoruser = $this->getDataGenerator()->create_user(); 346 347 $user2 = user::create( 348 $saveduser->get_resourceid(), 349 $instructoruser->id, 350 $saveduser->get_deploymentid(), 351 'another-user-123', 352 $CFG->lang, 353 '99', 354 'Perth', 355 'AU', 356 'An Example Institution', 357 2 358 ); 359 $saveduser2 = $userrepo->save($user2); 360 $savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2]; 361 362 $foundusers = $userrepo->find_by_resource($saveduser->get_resourceid()); 363 $this->assertCount(2, $foundusers); 364 foreach ($foundusers as $founduser) { 365 $this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser); 366 } 367 } 368 369 /** 370 * Test that users can be found based on their resource_link association. 371 * 372 * @covers ::find_by_resource_link 373 */ 374 public function test_find_by_resource_link() { 375 global $CFG; 376 $this->resetAfterTest(); 377 $user = $this->generate_user(); 378 $user->set_resourcelinkid(33); 379 $userrepo = new user_repository(); 380 $saveduser = $userrepo->save($user); 381 382 $instructoruser = $this->getDataGenerator()->create_user(); 383 $user2 = user::create( 384 $saveduser->get_resourceid(), 385 $instructoruser->id, 386 $saveduser->get_deploymentid(), 387 'another-user-123', 388 $CFG->lang, 389 '99', 390 'Perth', 391 'AU', 392 'An Example Institution', 393 2, 394 null, 395 null, 396 33 397 ); 398 $saveduser2 = $userrepo->save($user2); 399 $savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2]; 400 401 $foundusers = $userrepo->find_by_resource_link(33); 402 $this->assertCount(2, $foundusers); 403 foreach ($foundusers as $founduser) { 404 $this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser); 405 } 406 } 407 408 /** 409 * Test checking existence of a user instance, based on id. 410 * 411 * @covers ::exists 412 */ 413 public function test_exists() { 414 $this->resetAfterTest(); 415 $user = $this->generate_user(); 416 $userrepo = new user_repository(); 417 $saveduser = $userrepo->save($user); 418 419 $this->assertTrue($userrepo->exists($saveduser->get_id())); 420 $this->assertFalse($userrepo->exists(-50)); 421 } 422 423 /** 424 * Test deleting a user instance, based on id. 425 * 426 * @covers ::delete 427 */ 428 public function test_delete() { 429 $this->resetAfterTest(); 430 $user = $this->generate_user(); 431 $userrepo = new user_repository(); 432 $saveduser = $userrepo->save($user); 433 $this->assertTrue($userrepo->exists($saveduser->get_id())); 434 435 $userrepo->delete($saveduser->get_id()); 436 $this->assertFalse($userrepo->exists($saveduser->get_id())); 437 438 global $DB; 439 $this->assertFalse($DB->record_exists('enrol_lti_users', ['id' => $saveduser->get_id()])); 440 $this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['ltiuserid' => $saveduser->get_id()])); 441 $this->assertTrue($DB->record_exists('user', ['id' => $saveduser->get_localid()])); 442 443 $this->assertNull($userrepo->delete($saveduser->get_id())); 444 } 445 446 /** 447 * Test deleting a collection of lti user instances by deployment. 448 * 449 * @covers ::delete_by_deployment 450 */ 451 public function test_delete_by_deployment() { 452 global $CFG; 453 $this->resetAfterTest(); 454 $user = $this->generate_user(); 455 $userrepo = new user_repository(); 456 $saveduser = $userrepo->save($user); 457 $instructoruser = $this->getDataGenerator()->create_user(); 458 $instructor2user = $this->getDataGenerator()->create_user(); 459 460 $user2 = user::create( 461 $saveduser->get_resourceid(), 462 $instructoruser->id, 463 $saveduser->get_deploymentid(), 464 'another-user-123', 465 $CFG->lang, 466 '99', 467 'Perth', 468 'AU', 469 'An Example Institution', 470 ); 471 $saveduser2 = $userrepo->save($user2); 472 473 $user3 = user::create( 474 $saveduser->get_resourceid(), 475 $instructor2user->id, 476 $saveduser->get_deploymentid() + 1, 477 'another-user-678', 478 $CFG->lang, 479 '99', 480 'Melbourne', 481 'AU', 482 'An Example Institution', 483 ); 484 $saveduser3 = $userrepo->save($user3); 485 $this->assertTrue($userrepo->exists($saveduser->get_id())); 486 $this->assertTrue($userrepo->exists($saveduser2->get_id())); 487 $this->assertTrue($userrepo->exists($saveduser3->get_id())); 488 489 $userrepo->delete_by_deployment($saveduser->get_deploymentid()); 490 $this->assertFalse($userrepo->exists($saveduser->get_id())); 491 $this->assertFalse($userrepo->exists($saveduser2->get_id())); 492 $this->assertTrue($userrepo->exists($saveduser3->get_id())); 493 } 494 495 /** 496 * Verify a user who has been deleted can be re-saved to the repository and matched to an existing local user. 497 * 498 * @covers ::save 499 */ 500 public function test_save_deleted() { 501 $this->resetAfterTest(); 502 $user = $this->generate_user(); 503 $userrepo = new user_repository(); 504 $saveduser = $userrepo->save($user); 505 506 $userrepo->delete($saveduser->get_id()); 507 $this->assertFalse($userrepo->exists($saveduser->get_id())); 508 509 $saveduser2 = $userrepo->save($user); 510 $this->assertEquals($saveduser->get_localid(), $saveduser2->get_localid()); 511 $this->assertNotEquals($saveduser->get_id(), $saveduser2->get_id()); 512 } 513 514 /** 515 * Test confirming that any associated legacy lti user records are not returned by the repository. 516 * 517 * This test ensures that any enrolment methods (resources) updated in-place from legacy LTI to 1.3 only return LTI 1.3 users. 518 * 519 * @covers ::find 520 * @covers ::find_single_user_by_resource 521 * @covers ::find_by_resource 522 */ 523 public function test_find_filters_legacy_lti_users(): void { 524 $this->resetAfterTest(); 525 global $DB; 526 $user = $this->getDataGenerator()->create_user(); 527 $course = $this->getDataGenerator()->create_course(); 528 $resource = $this->getDataGenerator()->create_lti_tool((object)['courseid' => $course->id]); 529 $ltiuserdata = [ 530 'userid' => $user->id, 531 'toolid' => $resource->id, 532 'sourceid' => '1001', 533 ]; 534 $ltiuserid = $DB->insert_record('enrol_lti_users', $ltiuserdata); 535 $userrepo = new user_repository(); 536 537 $this->assertNull($userrepo->find($ltiuserid)); 538 $this->assertNull($userrepo->find_single_user_by_resource($user->id, $resource->id)); 539 $this->assertEmpty($userrepo->find_by_resource($resource->id)); 540 541 // Set deploymentid, indicating the user originated from an LTI 1.3 launch and should now be returned. 542 $ltiuserdata['id'] = $ltiuserid; 543 $ltiuserdata['ltideploymentid'] = '234'; 544 $DB->update_record('enrol_lti_users', $ltiuserdata); 545 546 $this->assertInstanceOf(user::class, $userrepo->find($ltiuserid)); 547 $this->assertInstanceOf(user::class, $userrepo->find_single_user_by_resource($user->id, $resource->id)); 548 $ltiusers = $userrepo->find_by_resource($resource->id); 549 $this->assertCount(1, $ltiusers); 550 $this->assertInstanceOf(user::class, reset($ltiusers)); 551 } 552 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body