Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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  }