See Release Notes
Long Term Support Release
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\task; 18 19 use enrol_lti\helper; 20 use enrol_lti\local\ltiadvantage\entity\user; 21 use enrol_lti\local\ltiadvantage\repository\resource_link_repository; 22 use enrol_lti\local\ltiadvantage\repository\user_repository; 23 24 defined('MOODLE_INTERNAL') || die(); 25 26 require_once (__DIR__ . '/../lti_advantage_testcase.php'); 27 28 /** 29 * Tests for the enrol_lti\local\ltiadvantage\task\sync_members scheduled task. 30 * 31 * @package enrol_lti 32 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_members 35 */ 36 class sync_members_test extends \lti_advantage_testcase { 37 38 /** 39 * Verify the user's profile picture has been set, which is useful to verify picture syncs. 40 * 41 * @param int $userid the id of the Moodle user. 42 * @param bool $match true to verify a match, false to verify a non-match. 43 */ 44 protected function verify_user_profile_image(int $userid, bool $match = true): void { 45 global $CFG; 46 $user = \core_user::get_user($userid); 47 $usercontext = \context_user::instance($user->id); 48 $expected = $CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev='. $user->picture; 49 50 $page = new \moodle_page(); 51 $page->set_url('/user/profile.php'); 52 $page->set_context(\context_system::instance()); 53 $renderer = $page->get_renderer('core'); 54 $userpicture = new \user_picture($user); 55 if ($match) { 56 $this->assertEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); 57 } else { 58 $this->assertNotEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); 59 } 60 61 } 62 63 /** 64 * Helper to get a list of mocked member entries for use in the mocked sync task. 65 * 66 * @param array $userids the array of lti user ids to use. 67 * @param array|null $legacyuserids legacy user ids for the lti11_legacy_user_id property, null if not desired. 68 * @param bool $names whether to include names in the user data or not. 69 * @param bool $emails whether to include email in the user data or not. 70 * @param bool $linklevel whether to mock the user return data at link-level (true) or context-level (false). 71 * @param bool $picture whether to mock a user's picture field in the return data. 72 * @param array $roles an array of IMS roles to include with each member which, if empty, defaults to just the learner role. 73 * @return array the array of users. 74 * @throws \Exception if the legacyuserids array doesn't contain the correct number of ids. 75 */ 76 protected function get_mock_members_with_ids(array $userids, ?array $legacyuserids = null, $names = true, 77 $emails = true, bool $linklevel = true, bool $picture = false, array $roles = []): array { 78 79 if (!is_null($legacyuserids) && count($legacyuserids) != count($userids)) { 80 throw new \Exception('legacyuserids must contain the same number of ids as $userids.'); 81 } 82 83 if (empty($roles)) { 84 $roles = ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner']; 85 } 86 87 $users = []; 88 foreach ($userids as $userid) { 89 $user = ['user_id' => (string) $userid, 'roles' => $roles]; 90 if ($picture) { 91 $user['picture'] = $this->getExternalTestFileUrl('/test.jpg', false); 92 } 93 if ($names) { 94 $user['given_name'] = 'Firstname' . $userid; 95 $user['family_name'] = 'Surname' . $userid; 96 } 97 if ($emails) { 98 $user['email'] = "firstname.surname{$userid}@lms.example.org"; 99 } 100 if ($legacyuserids) { 101 $user['lti11_legacy_user_id'] = array_shift($legacyuserids); 102 } 103 if ($linklevel) { 104 // Link-level memberships also include a message property. 105 $user['message'] = [ 106 'https://purl.imsglobal.org/spec/lti/claim/message_type' => 'LtiResourceLinkRequest' 107 ]; 108 } 109 $users[] = $user; 110 } 111 return $users; 112 } 113 114 /** 115 * Gets a task mocked to only support resource-link-level memberships request. 116 * 117 * @param array $resourcelinks array for stipulating per link users, containing list of [resourcelink, members]. 118 * @return sync_members|\PHPUnit\Framework\MockObject\MockObject 119 */ 120 protected function get_mock_task_resource_link_level(array $resourcelinks = []) { 121 $mocktask = $this->getMockBuilder(sync_members::class) 122 ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members']) 123 ->getMock(); 124 $mocktask->expects($this->any()) 125 ->method('get_context_level_members') 126 ->will($this->returnCallback(function() { 127 return false; 128 })); 129 $expectedcount = !empty($resourcelinks) ? count($resourcelinks) : 1; 130 $mocktask->expects($this->exactly($expectedcount)) 131 ->method('get_resource_link_level_members') 132 ->will($this->returnCallback(function ($nrpsinfo, $serviceconnector, $registration, $reslink) use ($resourcelinks) { 133 if ($resourcelinks) { 134 foreach ($resourcelinks as $rl) { 135 if ($reslink->get_resourcelinkid() === $rl[0]->get_resourcelinkid()) { 136 return $rl[1]; 137 } 138 } 139 } else { 140 return $this->get_mock_members_with_ids(range(1, 2)); 141 } 142 })); 143 return $mocktask; 144 } 145 146 /** 147 * Gets a task mocked to only support context-level memberships request. 148 * 149 * @return sync_members|\PHPUnit\Framework\MockObject\MockObject 150 */ 151 protected function get_mock_task_context_level() { 152 $mocktask = $this->getMockBuilder(sync_members::class) 153 ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members']) 154 ->getMock(); 155 $mocktask->expects($this->any()) 156 ->method('get_resource_link_level_members') 157 ->will($this->returnCallback(function() { 158 // An exception is what the service code will throw if the resource link level service isn't available. 159 throw new \Exception(); 160 })); 161 $mocktask->expects($this->any()) 162 ->method('get_context_level_members') 163 ->will($this->returnCallback(function() { 164 return $this->get_mock_members_with_ids(range(1, 3), null, true, true, false); 165 }));; 166 return $mocktask; 167 } 168 169 /** 170 * Gets a sync task, with the remote calls mocked to return the supplied users. 171 * 172 * See get_mock_members_with_ids() for generating the users for input. 173 * 174 * @param array $users a list of users, the result of a call to get_mock_members_with_ids(). 175 * @return \PHPUnit\Framework\MockObject\MockObject the mock task. 176 */ 177 protected function get_mock_task_with_users(array $users) { 178 $mocktask = $this->getMockBuilder(sync_members::class) 179 ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members']) 180 ->getMock(); 181 $mocktask->expects($this->any()) 182 ->method('get_context_level_members') 183 ->will($this->returnCallback(function() { 184 return false; 185 })); 186 $mocktask->expects($this->any()) 187 ->method('get_resource_link_level_members') 188 ->will($this->returnCallback(function () use ($users) { 189 return $users; 190 })); 191 return $mocktask; 192 } 193 194 /** 195 * Check that all the given ltiusers are enrolled in the course. 196 * 197 * @param \stdClass $course the course instance. 198 * @param user[] $ltiusers array of lti user instances. 199 */ 200 protected function verify_course_enrolments(\stdClass $course, array $ltiusers) { 201 global $CFG; 202 require_once($CFG->libdir . '/enrollib.php'); 203 $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); 204 $this->assertCount(count($ltiusers), $enrolledusers); 205 $enrolleduserids = array_map(function($stringid) { 206 return (int) $stringid; 207 }, array_column($enrolledusers, 'id')); 208 foreach ($ltiusers as $ltiuser) { 209 $this->assertContains($ltiuser->get_localid(), $enrolleduserids); 210 } 211 } 212 213 /** 214 * Test confirming task name. 215 * 216 * @covers ::get_name 217 */ 218 public function test_get_name() { 219 $this->assertEquals(get_string('tasksyncmembers', 'enrol_lti'), (new sync_members())->get_name()); 220 } 221 222 /** 223 * Test a resource-link-level membership sync, confirming that all relevant domain objects are updated properly. 224 * 225 * @covers ::execute 226 */ 227 public function test_resource_link_level_sync() { 228 $this->resetAfterTest(); 229 [$course, $resource] = $this->create_test_environment(); 230 231 // Launch the tool for a user. 232 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); 233 $instructoruser = $this->lti_advantage_user_authenticates('1'); 234 $launchservice = $this->get_tool_launch_service(); 235 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 236 237 // Sync members. 238 $task = $this->get_mock_task_resource_link_level(); 239 $task->execute(); 240 241 // Verify 2 users and their corresponding course enrolments exist. 242 $this->expectOutputRegex( 243 "/Completed - Synced members for tool '$resource->id' in the course '$course->id'. ". 244 "Processed 2 users; enrolled 2 members; unenrolled 0 members./" 245 ); 246 $userrepo = new user_repository(); 247 $ltiusers = $userrepo->find_by_resource($resource->id); 248 $this->assertCount(2, $ltiusers); 249 $this->verify_course_enrolments($course, $ltiusers); 250 } 251 252 /** 253 * Test a resource-link-level membership sync when there are more than one resource links for the resource. 254 * 255 * @covers ::execute 256 */ 257 public function test_resource_link_level_sync_multiple_resource_links() { 258 $this->resetAfterTest(); 259 [$course, $resource] = $this->create_test_environment(); 260 261 // Launch twice - once from each resource link in the platform. 262 $launchservice = $this->get_tool_launch_service(); 263 $instructoruser = $this->lti_advantage_user_authenticates('1'); 264 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0], '123'); 265 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 266 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0], '456'); 267 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 268 269 // Now, grab the resource links. 270 $rlrepo = new resource_link_repository(); 271 $reslinks = $rlrepo->find_by_resource($resource->id); 272 $mockmembers = $this->get_mock_members_with_ids(range(1, 10)); 273 $mockusers1 = array_slice($mockmembers, 0, 6); 274 $mockusers2 = array_slice($mockmembers, 6); 275 $resourcelinks = [ 276 [$reslinks[0], $mockusers1], 277 [$reslinks[1], $mockusers2] 278 ]; 279 280 // Sync the members, using the mock task set up to sync different sets of users for each resource link. 281 $task = $this->get_mock_task_resource_link_level($resourcelinks); 282 ob_start(); 283 $task->execute(); 284 $output = ob_get_contents(); 285 ob_end_clean(); 286 287 // Verify 10 users and their corresponding course enrolments exist. 288 $userrepo = new user_repository(); 289 $ltiusers = $userrepo->find_by_resource($resource->id); 290 $this->assertCount(10, $ltiusers); 291 $this->assertStringContainsString("Completed - Synced 6 members for the resource link", $output); 292 $this->assertStringContainsString("Completed - Synced 4 members for the resource link", $output); 293 $this->assertStringContainsString("Completed - Synced members for tool '$resource->id' in the course '". 294 "$resource->courseid'. Processed 10 users; enrolled 10 members; unenrolled 0 members.\n", $output); 295 $this->verify_course_enrolments($course, $ltiusers); 296 } 297 298 /** 299 * Verify the task will update users' profile pictures if the 'picture' member field is provided. 300 * 301 * @covers ::execute 302 */ 303 public function test_user_profile_image_sync() { 304 $this->resetAfterTest(); 305 [$course, $resource] = $this->create_test_environment(); 306 307 // Launch the tool for a user. 308 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); 309 $launchservice = $this->get_tool_launch_service(); 310 $instructoruser = $this->lti_advantage_user_authenticates('1'); 311 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 312 313 // Sync members. 314 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(['1'], null, true, true, true, true)); 315 ob_start(); 316 $task->execute(); 317 ob_end_clean(); 318 319 // Verify 1 users and their corresponding course enrolments exist. 320 $userrepo = new user_repository(); 321 $ltiusers = $userrepo->find_by_resource($resource->id); 322 $this->assertCount(1, $ltiusers); 323 $this->verify_course_enrolments($course, $ltiusers); 324 325 // Verify user profile image has been updated. 326 $this->verify_user_profile_image($ltiusers[0]->get_localid()); 327 } 328 329 /** 330 * Test a context-level membership sync, confirming that all relevant domain objects are updated properly. 331 * 332 * @covers ::execute 333 */ 334 public function test_context_level_sync() { 335 $this->resetAfterTest(); 336 [$course, $resource] = $this->create_test_environment(); 337 338 // Launch the tool for a user. 339 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); 340 $launchservice = $this->get_tool_launch_service(); 341 $instructoruser = $this->lti_advantage_user_authenticates('1'); 342 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 343 344 // Sync members. 345 $task = $this->get_mock_task_context_level(); 346 ob_start(); 347 $task->execute(); 348 ob_end_clean(); 349 350 // Verify 3 users and their corresponding course enrolments exist. 351 $userrepo = new user_repository(); 352 $ltiusers = $userrepo->find_by_resource($resource->id); 353 $this->assertCount(3, $ltiusers); 354 $this->verify_course_enrolments($course, $ltiusers); 355 } 356 357 /** 358 * Test verifying the sync task handles the omission/inclusion of PII information for users. 359 * 360 * @covers ::execute 361 */ 362 public function test_sync_user_data() { 363 $this->resetAfterTest(); 364 [$course, $resource, $resource2, $resource3, $appreg] = $this->create_test_environment(); 365 $userrepo = new user_repository(); 366 367 // Launch the tool for a user. 368 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); 369 $launchservice = $this->get_tool_launch_service(); 370 $instructoruser = $this->lti_advantage_user_authenticates('1'); 371 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 372 373 // Sync members. 374 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(1, 5), null, false, false)); 375 376 ob_start(); 377 $task->execute(); 378 ob_end_clean(); 379 380 // Verify 5 users and their corresponding course enrolments exist. 381 $ltiusers = $userrepo->find_by_resource($resource->id); 382 $this->assertCount(5, $ltiusers); 383 $this->verify_course_enrolments($course, $ltiusers); 384 385 // Since user data wasn't included in the response, the users will have been synced using fallbacks, 386 // so verify these. 387 foreach ($ltiusers as $ltiuser) { 388 $user = \core_user::get_user($ltiuser->get_localid()); 389 // Firstname falls back to sourceid. 390 $this->assertEquals($ltiuser->get_sourceid(), $user->firstname); 391 392 // Lastname falls back to resource context id. 393 $this->assertEquals($appreg->get_platformid(), $user->lastname); 394 395 // Email falls back to example.com. 396 $issuersubhash = sha1($appreg->get_platformid() . '_' . $ltiuser->get_sourceid()); 397 $this->assertEquals("enrol_lti_13_{$issuersubhash}@example.com", $user->email); 398 } 399 400 // Sync again, this time with user data included. 401 $mockmembers = $this->get_mock_members_with_ids(range(1, 5)); 402 $task = $this->get_mock_task_with_users($mockmembers); 403 404 ob_start(); 405 $task->execute(); 406 ob_end_clean(); 407 408 // User data was included in the response and should have been updated. 409 $ltiusers = $userrepo->find_by_resource($resource->id); 410 $this->assertCount(5, $ltiusers); 411 $this->verify_course_enrolments($course, $ltiusers); 412 foreach ($ltiusers as $ltiuser) { 413 $user = \core_user::get_user($ltiuser->get_localid()); 414 $mockmemberindex = array_search($ltiuser->get_sourceid(), array_column($mockmembers, 'user_id')); 415 $mockmember = $mockmembers[$mockmemberindex]; 416 $this->assertEquals($mockmember['given_name'], $user->firstname); 417 $this->assertEquals($mockmember['family_name'], $user->lastname); 418 $this->assertEquals($mockmember['email'], $user->email); 419 } 420 } 421 422 /** 423 * Test verifying the task won't sync members for shared resources having member sync disabled. 424 * 425 * @covers ::execute 426 */ 427 public function test_membership_sync_disabled() { 428 $this->resetAfterTest(); 429 [$course, $resource] = $this->create_test_environment(true, true, false); 430 431 // Launch the tool for a user. 432 $mockuser = $this->get_mock_launch_users_with_ids(['1'])[0]; 433 $mocklaunch = $this->get_mock_launch($resource, $mockuser); 434 $launchservice = $this->get_tool_launch_service(); 435 $instructoruser = $this->lti_advantage_user_authenticates('1'); 436 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 437 438 // Sync members. 439 $task = $this->get_mock_task_with_users($this->get_mock_launch_users_with_ids(range(1, 4))); 440 ob_start(); 441 $task->execute(); 442 ob_end_clean(); 443 444 // Verify no users were added or removed. 445 // A single user (the user who launched the resource link) is expected. 446 $userrepo = new user_repository(); 447 $ltiusers = $userrepo->find_by_resource($resource->id); 448 $this->assertCount(1, $ltiusers); 449 $this->assertEquals($mockuser['user_id'], $ltiusers[0]->get_sourceid()); 450 $this->verify_course_enrolments($course, $ltiusers); 451 } 452 453 /** 454 * Test verifying the sync task for resources configured as 'helper::MEMBER_SYNC_ENROL_AND_UNENROL'. 455 * 456 * @covers ::execute 457 */ 458 public function test_sync_mode_enrol_and_unenrol() { 459 $this->resetAfterTest(); 460 [$course, $resource] = $this->create_test_environment(); 461 $userrepo = new user_repository(); 462 463 // Launch the tool for a user. 464 $mockuser = $this->get_mock_launch_users_with_ids(['1'])[0]; 465 $mocklaunch = $this->get_mock_launch($resource, $mockuser); 466 $launchservice = $this->get_tool_launch_service(); 467 $instructoruser = $this->lti_advantage_user_authenticates('1'); 468 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 469 470 // Sync members. 471 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(1, 3))); 472 473 ob_start(); 474 $task->execute(); 475 ob_end_clean(); 476 477 // Verify 3 users and their corresponding course enrolments exist. 478 $ltiusers = $userrepo->find_by_resource($resource->id); 479 $this->assertCount(3, $ltiusers); 480 $this->verify_course_enrolments($course, $ltiusers); 481 482 // Now, simulate a subsequent sync in which 1 existing user maintains access, 483 // 2 existing users are unenrolled and 3 new users are enrolled. 484 $task2 = $this->get_mock_task_with_users($this->get_mock_members_with_ids(['1', '4', '5', '6'])); 485 ob_start(); 486 $task2->execute(); 487 ob_end_clean(); 488 489 // Verify the missing users have been unenrolled and new users enrolled. 490 $ltiusers = $userrepo->find_by_resource($resource->id); 491 $this->assertCount(4, $ltiusers); 492 $unenrolleduserids = ['2', '3']; 493 $enrolleduserids = ['1', '4', '5', '6']; 494 foreach ($ltiusers as $ltiuser) { 495 $this->assertNotContains($ltiuser->get_sourceid(), $unenrolleduserids); 496 $this->assertContains($ltiuser->get_sourceid(), $enrolleduserids); 497 } 498 $this->verify_course_enrolments($course, $ltiusers); 499 } 500 501 /** 502 * Confirm the sync task operation for resources configured as 'helper::MEMBER_SYNC_UNENROL_MISSING'. 503 * 504 * @covers ::execute 505 */ 506 public function test_sync_mode_unenrol_missing() { 507 $this->resetAfterTest(); 508 [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_UNENROL_MISSING); 509 $userrepo = new user_repository(); 510 511 // Launch the tool for a user. 512 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids([1])[0]); 513 $launchservice = $this->get_tool_launch_service(); 514 $instructoruser = $this->lti_advantage_user_authenticates('1'); 515 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 516 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 517 518 // Sync members using a payload which doesn't include the original launch user (User id = 1). 519 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(2, 3))); 520 521 ob_start(); 522 $task->execute(); 523 ob_end_clean(); 524 525 // Verify the original user (launching user) has been unenrolled and that no new members have been enrolled. 526 $ltiusers = $userrepo->find_by_resource($resource->id); 527 $this->assertCount(0, $ltiusers); 528 } 529 530 /** 531 * Confirm the sync task operation for resources configured as 'helper::MEMBER_SYNC_ENROL_NEW'. 532 * 533 * @covers ::execute 534 */ 535 public function test_sync_mode_enrol_new() { 536 $this->resetAfterTest(); 537 [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_NEW); 538 $userrepo = new user_repository(); 539 540 // Launch the tool for a user. 541 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids([1])[0]); 542 $launchservice = $this->get_tool_launch_service(); 543 $instructoruser = $this->lti_advantage_user_authenticates('1'); 544 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 545 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 546 547 // Sync members using a payload which includes two new members only (i.e. not the original launching user). 548 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(2, 3))); 549 550 ob_start(); 551 $task->execute(); 552 ob_end_clean(); 553 554 // Verify we now have 3 enrolments. The original user (who was not unenrolled) and the 2 new users. 555 $ltiusers = $userrepo->find_by_resource($resource->id); 556 $this->assertCount(3, $ltiusers); 557 $this->verify_course_enrolments($course, $ltiusers); 558 } 559 560 /** 561 * Test confirming that no changes take place if the auth_lti plugin is not enabled. 562 * 563 * @covers ::execute 564 */ 565 public function test_sync_auth_disabled() { 566 $this->resetAfterTest(); 567 [$course, $resource] = $this->create_test_environment(false); 568 $userrepo = new user_repository(); 569 570 // Launch the tool for a user. 571 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids([1])[0]); 572 $launchservice = $this->get_tool_launch_service(); 573 $instructoruser = $this->lti_advantage_user_authenticates('1'); 574 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 575 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 576 577 // If the task were to run, this would trigger 1 unenrolment (the launching user) and 3 enrolments. 578 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(2, 2))); 579 $task->execute(); 580 581 // Verify that the sync didn't take place. 582 $this->expectOutputRegex("/Skipping task - Authentication plugin 'LTI' is not enabled/"); 583 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 584 } 585 586 /** 587 * Test confirming that no sync takes place when the enrol_lti plugin is not enabled. 588 * 589 * @covers ::execute 590 */ 591 public function test_sync_enrol_disabled() { 592 $this->resetAfterTest(); 593 [$course, $resource] = $this->create_test_environment(true, false); 594 $userrepo = new user_repository(); 595 596 // Launch the tool for a user. 597 $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids([1])[0]); 598 $launchservice = $this->get_tool_launch_service(); 599 $instructoruser = $this->lti_advantage_user_authenticates('1'); 600 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 601 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 602 603 // If the task were to run, this would trigger 1 unenrolment of the launching user and enrolment of 3 users. 604 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(2, 2))); 605 $task->execute(); 606 607 // Verify that the sync didn't take place. 608 $this->expectOutputRegex("/Skipping task - The 'Publish as LTI tool' plugin is disabled/"); 609 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 610 } 611 612 /** 613 * Test syncing members when the enrolment instance is disabled. 614 * 615 * @covers ::execute 616 */ 617 public function test_sync_members_disabled_instance() { 618 $this->resetAfterTest(); 619 global $DB; 620 621 [$course, $resource, $resource2, $resource3] = $this->create_test_environment(); 622 $userrepo = new user_repository(); 623 624 // Disable resource 1. 625 $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED]; 626 $DB->update_record('enrol', $enrol); 627 628 // Delete the activity being shared by resource2, leaving resource 2 disabled as a result. 629 $modcontext = \context::instance_by_id($resource2->contextid); 630 course_delete_module($modcontext->instanceid); 631 632 // Only the enabled resource 3 should sync members. 633 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(1, 1))); 634 $task->execute(); 635 636 $this->expectOutputRegex( 637 "/^Starting - Member sync for published resource '$resource3->id' for course '$course->id'.\n". 638 "Completed - Synced members for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ". 639 "enrolled 0 members; unenrolled 0 members.\n$/" 640 ); 641 $this->assertCount(0, $userrepo->find_by_resource($resource->id)); 642 } 643 644 /** 645 * Test syncing members for a membersync-enabled resource when the launch omits the NRPS service endpoints. 646 * 647 * @covers ::execute 648 */ 649 public function test_sync_no_nrps_support() { 650 $this->resetAfterTest(); 651 [$course, $resource] = $this->create_test_environment(); 652 $userrepo = new user_repository(); 653 654 // Launch the tool for a user. 655 $mockinstructor = $this->get_mock_launch_users_with_ids([1])[0]; 656 $mocklaunch = $this->get_mock_launch($resource, $mockinstructor, null, null, false); 657 $launchservice = $this->get_tool_launch_service(); 658 $instructoruser = $this->lti_advantage_user_authenticates('1'); 659 $launchservice->user_launches_tool($instructoruser, $mocklaunch); 660 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 661 662 // The task would sync an additional 2 users if the link had NRPS service support. 663 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(range(2, 2))); 664 665 // We expect the task to report that it is skipping the resource due to a lack of NRPS support. 666 $task->execute(); 667 668 // Verify no enrolments or unenrolments. 669 $this->expectOutputRegex( 670 "/Skipping - No names and roles service found.\n". 671 "Completed - Synced members for tool '{$resource->id}' in the course '{$course->id}'. ". 672 "Processed 0 users; enrolled 0 members; unenrolled 0 members./" 673 ); 674 $this->assertCount(1, $userrepo->find_by_resource($resource->id)); 675 } 676 677 /** 678 * Test confirming that preexisting, non-lti user accounts do not have their profiles or pictures updated during sync. 679 * 680 * @covers ::execute 681 */ 682 public function test_sync_non_lti_linked_user() { 683 $this->resetAfterTest(); 684 685 // Set up the environment. 686 [$course, $resource] = $this->create_test_environment(); 687 688 // Fake an auth - making sure it's a manual account. 689 $authenticateduser = $this->lti_advantage_user_authenticates('123'); 690 $authenticateduser->auth = 'manual'; 691 $authenticateduser->password = '1234abcD*'; 692 user_update_user($authenticateduser); 693 $authenticateduser = \core_user::get_user($authenticateduser->id); 694 695 // Mock the launch for the specified user. 696 $mocklaunchuser = $this->get_mock_launch_users_with_ids([$authenticateduser->id])[0]; 697 $mocklaunch = $this->get_mock_launch($resource, $mocklaunchuser); 698 $this->get_tool_launch_service()->user_launches_tool($authenticateduser, $mocklaunch); 699 700 // Prepare the sync task, with a stubbed list of members. 701 $task = $this->get_mock_task_with_users($this->get_mock_members_with_ids(['123'], null, true, true, true, true)); 702 703 // Run the member sync. 704 $this->expectOutputRegex( 705 "/Skipped profile sync for user '$authenticateduser->id'. The user does not belong to the LTI auth method.\n" . 706 "Skipped picture sync for user '$authenticateduser->id'. The user does not belong to the LTI auth method/" 707 ); 708 $task->execute(); 709 710 $updateduser = \core_user::get_user($authenticateduser->id); 711 $this->assertEquals($authenticateduser->firstname, $updateduser->firstname); 712 $this->assertEquals($authenticateduser->lastname, $updateduser->lastname); 713 $this->assertEquals($authenticateduser->email, $updateduser->email); 714 $this->verify_user_profile_image($authenticateduser->id, false); 715 } 716 717 /** 718 * Test the member sync for a range of scenarios including migrated tools, unlaunched tools, provisioning methods. 719 * 720 * @dataProvider member_sync_data_provider 721 * @param array|null $legacydata array detailing what legacy information to create, or null if not required. 722 * @param array|null $resourceconfig array detailing config values to be used when creating the test enrol_lti instances. 723 * @param array $launchdata array containing details of the launch, including user and migration claim. 724 * @param array|null $syncmembers the members to use in the mock sync. 725 * @param array $expected the array detailing expectations. 726 * @covers ::execute 727 */ 728 public function test_sync_enrolments_and_migration(?array $legacydata, ?array $resourceconfig, array $launchdata, 729 ?array $syncmembers, array $expected) { 730 731 $this->resetAfterTest(); 732 733 // Set up the environment. 734 [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, false, 735 0, $resourceconfig['provisioningmodeinstructor'] ?? 0, $resourceconfig['provisioningmodelearner'] ?? 0); 736 737 // Set up legacy tool and user data. 738 if ($legacydata) { 739 [$legacytools, $legacyconsumerrecord, $legacyusers] = $this->setup_legacy_data($course, $legacydata); 740 } 741 742 // Mock the launch for the specified user. 743 $mocklaunch = $this->get_mock_launch($resource, $launchdata['user'], null, [], true, 744 $launchdata['launch_migration_claim']); 745 746 // Perform the launch. 747 $instructoruser = $this->lti_advantage_user_authenticates( 748 $launchdata['user']['user_id'], 749 $launchdata['launch_migration_claim'] ?? [] 750 ); 751 $this->get_tool_launch_service()->user_launches_tool($instructoruser, $mocklaunch); 752 753 // Prepare the sync task, with a stubbed list of members. 754 $task = $this->get_mock_task_with_users($syncmembers); 755 756 // Run the member sync. 757 ob_start(); 758 $task->execute(); 759 ob_end_clean(); 760 761 // Verify enrolments. 762 $ltiusers = (new user_repository())->find_by_resource($resource->id); 763 $enrolled = array_filter($expected['enrolments'], function($user) { 764 return $user['is_enrolled']; 765 }); 766 $this->assertCount(count($enrolled), $ltiusers); 767 $this->verify_course_enrolments($course, $ltiusers); 768 769 // Verify migration, if expected. 770 if ($legacydata) { 771 $legacyuserids = array_column($legacyusers, 'id'); 772 foreach ($ltiusers as $ltiuser) { 773 $this->assertArrayHasKey($ltiuser->get_sourceid(), $expected['enrolments']); 774 if (!$expected['enrolments'][$ltiuser->get_sourceid()]['is_migrated']) { 775 // Those members who hadn't launched over 1p1 prior will have new lti user records created. 776 $this->assertNotContains((string)$ltiuser->get_localid(), $legacyuserids); 777 } else { 778 // Those members who were either already migrated during launch, or were migrated during the sync, 779 // will be mapped to their legacy user accounts. 780 $this->assertContains((string)$ltiuser->get_localid(), $legacyuserids); 781 } 782 } 783 } 784 } 785 786 /** 787 * Data provider for member syncs. 788 * 789 * @return array[] the array of test data. 790 */ 791 public function member_sync_data_provider(): array { 792 global $CFG; 793 require_once($CFG->dirroot . '/auth/lti/auth.php'); 794 return [ 795 'Migrated tool, user ids changed, new and existing users present in sync' => [ 796 'legacy_data' => [ 797 'users' => [ 798 ['user_id' => '1'], 799 ['user_id' => '2'], 800 ], 801 'consumer_key' => 'CONSUMER_1', 802 'tools' => [ 803 ['secret' => 'toolsecret1'], 804 ['secret' => 'toolsecret2'], 805 ] 806 ], 807 'resource_config' => null, 808 'launch_data' => [ 809 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 810 'launch_migration_claim' => [ 811 'consumer_key' => 'CONSUMER_1', 812 'signing_secret' => 'toolsecret1', 813 'user_id' => '1', 814 'context_id' => 'd345b', 815 'tool_consumer_instance_guid' => '12345-123', 816 'resource_link_id' => '4b6fa' 817 ], 818 ], 819 'sync_members_data' => [ 820 $this->get_mock_members_with_ids(['1p3_1'], ['1'])[0], 821 $this->get_mock_members_with_ids(['1p3_2'], ['2'])[0], 822 $this->get_mock_members_with_ids(['1p3_3'], ['3'])[0], 823 $this->get_mock_members_with_ids(['1p3_4'], ['4'])[0], 824 ], 825 'expected' => [ 826 'enrolments' => [ 827 '1p3_1' => [ 828 'is_enrolled' => true, 829 'is_migrated' => true, 830 ], 831 '1p3_2' => [ 832 'is_enrolled' => true, 833 'is_migrated' => true, 834 ], 835 '1p3_3' => [ 836 'is_enrolled' => true, 837 'is_migrated' => false, 838 ], 839 '1p3_4' => [ 840 'is_enrolled' => true, 841 'is_migrated' => false, 842 ] 843 ] 844 ] 845 ], 846 'Migrated tool, no change in user ids, new and existing users present in sync' => [ 847 'legacy_data' => [ 848 'users' => [ 849 ['user_id' => '1'], 850 ['user_id' => '2'], 851 ], 852 'consumer_key' => 'CONSUMER_1', 853 'tools' => [ 854 ['secret' => 'toolsecret1'], 855 ['secret' => 'toolsecret2'], 856 ] 857 ], 858 'resource_config' => null, 859 'launch_data' => [ 860 'user' => $this->get_mock_launch_users_with_ids(['1'])[0], 861 'launch_migration_claim' => [ 862 'consumer_key' => 'CONSUMER_1', 863 'signing_secret' => 'toolsecret1', 864 'context_id' => 'd345b', 865 'tool_consumer_instance_guid' => '12345-123', 866 'resource_link_id' => '4b6fa' 867 ], 868 ], 869 'sync_members_data' => [ 870 $this->get_mock_members_with_ids(['1'], null)[0], 871 $this->get_mock_members_with_ids(['2'], null)[0], 872 $this->get_mock_members_with_ids(['3'], null)[0], 873 $this->get_mock_members_with_ids(['4'], null)[0], 874 ], 875 'expected' => [ 876 'enrolments' => [ 877 '1' => [ 878 'is_enrolled' => true, 879 'is_migrated' => true, 880 ], 881 '2' => [ 882 'is_enrolled' => true, 883 'is_migrated' => true, 884 ], 885 '3' => [ 886 'is_enrolled' => true, 887 'is_migrated' => false, 888 ], 889 '4' => [ 890 'is_enrolled' => true, 891 'is_migrated' => false, 892 ] 893 ] 894 ] 895 ], 896 'New tool, no launch migration claim, change in user ids, new and existing users present in sync' => [ 897 'legacy_data' => [ 898 'users' => [ 899 ['user_id' => '1'], 900 ['user_id' => '2'], 901 ], 902 'consumer_key' => 'CONSUMER_1', 903 'tools' => [ 904 ['secret' => 'toolsecret1'], 905 ['secret' => 'toolsecret2'], 906 ] 907 ], 908 'resource_config' => null, 909 'launch_data' => [ 910 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 911 'launch_migration_claim' => null, 912 ], 913 'sync_members_data' => [ 914 $this->get_mock_members_with_ids(['1p3_1'], null)[0], 915 $this->get_mock_members_with_ids(['1p3_2'], null)[0], 916 $this->get_mock_members_with_ids(['1p3_3'], null)[0], 917 $this->get_mock_members_with_ids(['1p3_4'], null)[0], 918 ], 919 'expected' => [ 920 'enrolments' => [ 921 '1p3_1' => [ 922 'is_enrolled' => true, 923 'is_migrated' => false, 924 ], 925 '1p3_2' => [ 926 'is_enrolled' => true, 927 'is_migrated' => false, 928 ], 929 '1p3_3' => [ 930 'is_enrolled' => true, 931 'is_migrated' => false, 932 ], 933 '1p3_4' => [ 934 'is_enrolled' => true, 935 'is_migrated' => false, 936 ] 937 ] 938 ] 939 ], 940 'New tool, no launch migration claim, no change in user ids, new and existing users present in sync' => [ 941 'legacy_data' => [ 942 'users' => [ 943 ['user_id' => '1'], 944 ['user_id' => '2'], 945 ], 946 'consumer_key' => 'CONSUMER_1', 947 'tools' => [ 948 ['secret' => 'toolsecret1'], 949 ['secret' => 'toolsecret2'], 950 ] 951 ], 952 'resource_config' => null, 953 'launch_data' => [ 954 'user' => $this->get_mock_launch_users_with_ids(['1'])[0], 955 'launch_migration_claim' => null, 956 ], 957 'sync_members_data' => [ 958 $this->get_mock_members_with_ids(['1'], null)[0], 959 $this->get_mock_members_with_ids(['2'], null)[0], 960 $this->get_mock_members_with_ids(['3'], null)[0], 961 $this->get_mock_members_with_ids(['4'], null)[0], 962 ], 963 'expected' => [ 964 'enrolments' => [ 965 '1' => [ 966 'is_enrolled' => true, 967 'is_migrated' => false, 968 ], 969 '2' => [ 970 'is_enrolled' => true, 971 'is_migrated' => false, 972 ], 973 '3' => [ 974 'is_enrolled' => true, 975 'is_migrated' => false, 976 ], 977 '4' => [ 978 'is_enrolled' => true, 979 'is_migrated' => false, 980 ] 981 ] 982 ] 983 ], 984 'New tool, migration only via member sync, no launch claim, new and existing users present in sync' => [ 985 'legacy_data' => [ 986 'users' => [ 987 ['user_id' => '1'], 988 ['user_id' => '2'], 989 ], 990 'consumer_key' => 'CONSUMER_1', 991 'tools' => [ 992 ['secret' => 'toolsecret1'], 993 ['secret' => 'toolsecret2'], 994 ] 995 ], 996 'resource_config' => null, 997 'launch_data' => [ 998 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 999 'launch_migration_claim' => null, 1000 ], 1001 'sync_members_data' => [ 1002 $this->get_mock_members_with_ids(['1p3_1'], ['1'])[0], 1003 $this->get_mock_members_with_ids(['1p3_2'], ['2'])[0], 1004 $this->get_mock_members_with_ids(['1p3_3'], ['3'])[0], 1005 $this->get_mock_members_with_ids(['1p3_4'], ['4'])[0], 1006 ], 1007 'expected' => [ 1008 'enrolments' => [ 1009 '1p3_1' => [ 1010 'is_enrolled' => true, 1011 'is_migrated' => false, 1012 ], 1013 '1p3_2' => [ 1014 'is_enrolled' => true, 1015 'is_migrated' => false, 1016 ], 1017 '1p3_3' => [ 1018 'is_enrolled' => true, 1019 'is_migrated' => false, 1020 ], 1021 '1p3_4' => [ 1022 'is_enrolled' => true, 1023 'is_migrated' => false, 1024 ] 1025 ] 1026 ] 1027 ], 1028 'Default provisioning modes, mixed bag of users and roles' => [ 1029 'legacy_data' => null, 1030 'resource_config' => [ 1031 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY, 1032 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING 1033 ], 1034 'launch_data' => [ 1035 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 1036 'launch_migration_claim' => null, 1037 ], 1038 'sync_members_data' => [ 1039 // This user is just an instructor but is also the user who is already linked, via the launch above. 1040 $this->get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 1041 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1042 ])[0], 1043 // This user is just a learner. 1044 $this->get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [ 1045 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1046 ])[0], 1047 // This user is also a learner. 1048 $this->get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [ 1049 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1050 ])[0], 1051 // This user is both an instructor and a learner. 1052 $this->get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [ 1053 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1054 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1055 ])[0], 1056 ], 1057 'expected' => [ 1058 'enrolments' => [ 1059 '1p3_1' => [ 1060 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 1061 'is_migrated' => false, 1062 ], 1063 '1p3_2' => [ 1064 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 1065 'is_migrated' => false, 1066 ], 1067 '1p3_3' => [ 1068 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 1069 'is_migrated' => false, 1070 ], 1071 '1p3_4' => [ 1072 'is_enrolled' => false, // Both roles - not enrolled due to instructor's 'prompt' provisioning mode. 1073 'is_migrated' => false, 1074 ] 1075 ] 1076 ] 1077 ], 1078 'All automatic provisioning, mixed bag of users and roles' => [ 1079 'legacy_data' => null, 1080 'resource_config' => [ 1081 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY, 1082 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY 1083 ], 1084 'launch_data' => [ 1085 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 1086 'launch_migration_claim' => null, 1087 ], 1088 'sync_members_data' => [ 1089 // This user is just an instructor but is also the user who is already linked, via the launch above. 1090 $this->get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 1091 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1092 ])[0], 1093 // This user is just a learner. 1094 $this->get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [ 1095 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1096 ])[0], 1097 // This user is also a learner. 1098 $this->get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [ 1099 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1100 ])[0], 1101 // This user is both an instructor and a learner. 1102 $this->get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [ 1103 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1104 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1105 ])[0], 1106 ], 1107 'expected' => [ 1108 'enrolments' => [ 1109 '1p3_1' => [ 1110 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 1111 'is_migrated' => false, 1112 ], 1113 '1p3_2' => [ 1114 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 1115 'is_migrated' => false, 1116 ], 1117 '1p3_3' => [ 1118 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 1119 'is_migrated' => false, 1120 ], 1121 '1p3_4' => [ 1122 'is_enrolled' => true, // Both roles - enrolled due to instructor's 'auto' provisioning mode. 1123 'is_migrated' => false, 1124 ] 1125 ] 1126 ] 1127 ], 1128 'All prompt provisioning, mixed bag of users and roles' => [ 1129 'legacy_data' => null, 1130 'resource_config' => [ 1131 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 1132 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING 1133 ], 1134 'launch_data' => [ 1135 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 1136 'launch_migration_claim' => null, 1137 ], 1138 'sync_members_data' => [ 1139 // This user is just an instructor but is also the user who is already linked, via the launch above. 1140 $this->get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 1141 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1142 ])[0], 1143 // This user is just a learner. 1144 $this->get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [ 1145 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1146 ])[0], 1147 // This user is also a learner. 1148 $this->get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [ 1149 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1150 ])[0], 1151 // This user is both an instructor and a learner. 1152 $this->get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [ 1153 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1154 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1155 ])[0], 1156 ], 1157 'expected' => [ 1158 'enrolments' => [ 1159 '1p3_1' => [ 1160 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 1161 'is_migrated' => false, 1162 ], 1163 '1p3_2' => [ 1164 'is_enrolled' => false, // Learner - not enrolled due to 'prompt' provisioning mode. 1165 'is_migrated' => false, 1166 ], 1167 '1p3_3' => [ 1168 'is_enrolled' => false, // Learner - not enrolled due to 'prompt' provisioning mode. 1169 'is_migrated' => false, 1170 ], 1171 '1p3_4' => [ 1172 'is_enrolled' => false, // Both roles - not enrolled due to instructor's 'prompt' provisioning mode. 1173 'is_migrated' => false, 1174 ] 1175 ] 1176 ] 1177 ], 1178 'All automatic provisioning, with legacy data and migration claim, mixed bag of users and roles' => [ 1179 'legacy_data' => [ 1180 'users' => [ 1181 ['user_id' => '2'], 1182 ['user_id' => '3'], 1183 ['user_id' => '4'], 1184 ['user_id' => '5'] 1185 ], 1186 'consumer_key' => 'CONSUMER_1', 1187 'tools' => [ 1188 ['secret' => 'toolsecret1'], 1189 ['secret' => 'toolsecret2'], 1190 ] 1191 ], 1192 'resource_config' => [ 1193 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY, 1194 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY 1195 ], 1196 'launch_data' => [ 1197 'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0], 1198 'launch_migration_claim' => [ 1199 'consumer_key' => 'CONSUMER_1', 1200 'signing_secret' => 'toolsecret1', 1201 'context_id' => 'd345b', 1202 'tool_consumer_instance_guid' => '12345-123', 1203 'resource_link_id' => '4b6fa' 1204 ], 1205 ], 1206 'sync_members_data' => [ 1207 // This user is just an instructor but is also the user who is already linked, via the launch above. 1208 $this->get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 1209 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1210 ])[0], 1211 // This user is just a learner. 1212 $this->get_mock_members_with_ids(['1p3_2'], ['2'], true, true, true, false, [ 1213 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1214 ])[0], 1215 // This user is also a learner. 1216 $this->get_mock_members_with_ids(['1p3_3'], ['3'], true, true, true, false, [ 1217 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1218 ])[0], 1219 // This user is both an instructor and a learner. 1220 $this->get_mock_members_with_ids(['1p3_4'], ['4'], true, true, true, false, [ 1221 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1222 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' 1223 ])[0], 1224 // This user is just an instructor who hasn't launched before (unlike the first user here). 1225 $this->get_mock_members_with_ids(['1p3_5'], ['5'], true, true, true, false, [ 1226 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 1227 ])[0], 1228 ], 1229 'expected' => [ 1230 'enrolments' => [ 1231 '1p3_1' => [ 1232 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 1233 'is_migrated' => false, 1234 ], 1235 '1p3_2' => [ 1236 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 1237 'is_migrated' => true, 1238 ], 1239 '1p3_3' => [ 1240 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 1241 'is_migrated' => true, 1242 ], 1243 '1p3_4' => [ 1244 'is_enrolled' => true, // Both roles - enrolled due to instructor's 'auto' provisioning mode. 1245 'is_migrated' => true 1246 ], 1247 '1p3_5' => [ 1248 'is_enrolled' => true, // Instructor role only - enrolled due to instructor's 'auto' provisioning mode. 1249 'is_migrated' => true 1250 ] 1251 ] 1252 ] 1253 ], 1254 ]; 1255 } 1256 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body