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\service;
  18  
  19  use core_availability\info_module;
  20  use enrol_lti\local\ltiadvantage\entity\resource_link;
  21  use enrol_lti\local\ltiadvantage\entity\user;
  22  use enrol_lti\local\ltiadvantage\entity\context;
  23  use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
  24  use enrol_lti\local\ltiadvantage\repository\context_repository;
  25  use enrol_lti\local\ltiadvantage\repository\deployment_repository;
  26  use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
  27  use enrol_lti\local\ltiadvantage\repository\user_repository;
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  require_once (__DIR__ . '/../lti_advantage_testcase.php');
  32  
  33  /**
  34   * Tests for the tool_launch_service.
  35   *
  36   * @package enrol_lti
  37   * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
  38   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   * @coversDefaultClass \enrol_lti\local\ltiadvantage\service\tool_launch_service
  40   */
  41  class tool_launch_service_test extends \lti_advantage_testcase {
  42  
  43      /**
  44       * Test the use case "A user launches a tool so they can view an external resource/activity".
  45       *
  46       * @dataProvider user_launch_provider
  47       * @param array|null $legacydata array detailing what legacy information to create, or null if not required.
  48       * @param array|null $launchdata array containing details of the launch, including user and migration claim.
  49       * @param array $expected the array detailing expectations.
  50       * @covers ::user_launches_tool
  51       */
  52      public function test_user_launches_tool(?array $legacydata, ?array $launchdata, array $expected) {
  53          $this->resetAfterTest();
  54          // Setup.
  55          $contextrepo = new context_repository();
  56          $resourcelinkrepo = new resource_link_repository();
  57          $deploymentrepo = new deployment_repository();
  58          $userrepo = new user_repository();
  59          [
  60              $course,
  61              $modresource,
  62              $modresource2,
  63              $courseresource,
  64              $registration,
  65              $deployment
  66          ] = $this->create_test_environment();
  67          $instructoruser = $this->getDataGenerator()->create_user();
  68  
  69          // Generate the legacy data, on which the user migration is based.
  70          if ($legacydata) {
  71              [$legacytools, $legacyconsumer, $legacyusers] = $this->setup_legacy_data($course, $legacydata);
  72          }
  73  
  74          // Get a mock 1.3 launch, optionally including the lti1p1 migration claim based on a legacy tool secret.
  75          $mocklaunch = $this->get_mock_launch($modresource, $launchdata['user'], null, [], true,
  76              $launchdata['launch_migration_claim']);
  77  
  78          // Call the service.
  79          $launchservice = $this->get_tool_launch_service();
  80          if (isset($expected['exception'])) {
  81              $this->expectException($expected['exception']);
  82              $this->expectExceptionMessage($expected['exception_message']);
  83          }
  84          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
  85  
  86          // As part of the launch, we expect to now have an lti-enrolled user who is recorded against the deployment.
  87          $users = $userrepo->find_by_resource($resource->id);
  88          $this->assertCount(1, $users);
  89          $user = array_pop($users);
  90          $this->assertInstanceOf(user::class, $user);
  91          $this->assertEquals($deployment->get_id(), $user->get_deploymentid());
  92  
  93          // Deployment should be mapped to the legacy consumer key even if the user wasn't matched and migrated.
  94          $updateddeployment = $deploymentrepo->find($deployment->get_id());
  95          $this->assertEquals($expected['deployment_consumer_key'], $updateddeployment->get_legacy_consumer_key());
  96  
  97          // The user comes from a resource_link, details of which should also be saved and linked to the deployment.
  98          $resourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id());
  99          $this->assertCount(1, $resourcelinks);
 100          $resourcelink = array_pop($resourcelinks);
 101          $this->assertInstanceOf(resource_link::class, $resourcelink);
 102          $this->assertEquals($deployment->get_id(), $resourcelink->get_deploymentid());
 103  
 104          // The resourcelink should have a context, which should also be saved and linked to the deployment.
 105          $context = $contextrepo->find($resourcelink->get_contextid());
 106          $this->assertInstanceOf(context::class, $context);
 107          $this->assertEquals($deployment->get_id(), $context->get_deploymentid());
 108  
 109          $enrolledusers = get_enrolled_users(\context_course::instance($course->id));
 110          $this->assertCount(1, $enrolledusers);
 111  
 112          // Verify the module is visible to the user.
 113          $cmcontext = \context::instance_by_id($modresource->contextid);
 114          $this->assertTrue(info_module::is_user_visible($cmcontext->instanceid, $userid));
 115  
 116          // And that other published modules are not yet visible to the user.
 117          $cmcontext = \context::instance_by_id($modresource2->contextid);
 118          $this->assertFalse(info_module::is_user_visible($cmcontext->instanceid, $userid));
 119      }
 120  
 121      /**
 122       * Provider for user launch testing.
 123       *
 124       * @return array the test case data.
 125       */
 126      public function user_launch_provider(): array {
 127          return [
 128              'New tool: no legacy data, no migration claim sent' => [
 129                  'legacy_data' => null,
 130                  'launch_data' => [
 131                      'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
 132                      'launch_migration_claim' => null,
 133                  ],
 134                  'expected' => [
 135                      'deployment_consumer_key' => null,
 136                  ]
 137              ],
 138              'Migrated tool: Legacy data exists, no change in user_id so omitted from claim' => [
 139                  'legacy_data' => [
 140                      'consumer_key' => 'CONSUMER_1',
 141                      'tools' => [
 142                          ['secret' => 'toolsecret1'],
 143                          ['secret' => 'toolsecret2'],
 144                      ]
 145                  ],
 146                  'launch_data' => [
 147                      'user' => $this->get_mock_launch_users_with_ids(['1'])[0],
 148                      'launch_migration_claim' => [
 149                          'consumer_key' => 'CONSUMER_1',
 150                          'signing_secret' => 'toolsecret1',
 151                          'context_id' => 'd345b',
 152                          'tool_consumer_instance_guid' => '12345-123',
 153                          'resource_link_id' => '4b6fa'
 154                      ],
 155                  ],
 156                  'expected' => [
 157                      'deployment_consumer_key' => 'CONSUMER_1',
 158                  ]
 159              ],
 160  
 161              'Migrated tool: Legacy data exists, platform signs with different valid secret' => [
 162                  'legacy_data' => [
 163                      'consumer_key' => 'CONSUMER_1',
 164                      'tools' => [
 165                          ['secret' => 'toolsecret1'],
 166                          ['secret' => 'toolsecret2'],
 167                      ]
 168                  ],
 169                  'launch_data' => [
 170                      'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
 171                      'launch_migration_claim' => [
 172                          'consumer_key' => 'CONSUMER_1',
 173                          'signing_secret' => 'toolsecret2',
 174                          'context_id' => 'd345b',
 175                          'tool_consumer_instance_guid' => '12345-123',
 176                          'resource_link_id' => '4b6fa'
 177                      ],
 178                  ],
 179                  'expected' => [
 180                      'deployment_consumer_key' => 'CONSUMER_1',
 181                  ]
 182              ],
 183              'Migrated tool: Legacy data exists, no migration claim sent' => [
 184                  'legacy_data' => [
 185                      'consumer_key' => 'CONSUMER_1',
 186                      'tools' => [
 187                          ['secret' => 'toolsecret1'],
 188                          ['secret' => 'toolsecret2'],
 189                      ]
 190                  ],
 191                  'launch_data' => [
 192                      'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
 193                      'launch_migration_claim' => null,
 194                  ],
 195                  'expected' => [
 196                      'deployment_consumer_key' => null,
 197                  ]
 198              ],
 199              'Migrated tool: Legacy data exists, migration claim signature generated using invalid secret' => [
 200                  'legacy_data' => [
 201                      'consumer_key' => 'CONSUMER_1',
 202                      'tools' => [
 203                          ['secret' => 'toolsecret1'],
 204                          ['secret' => 'toolsecret2'],
 205                      ]
 206                  ],
 207                  'launch_data' => [
 208                      'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
 209                      'launch_migration_claim' => [
 210                          'consumer_key' => 'CONSUMER_1',
 211                          'signing_secret' => 'secret-not-mapped-to-consumer',
 212                          'user_id' => 'user-id-123',
 213                          'context_id' => 'd345b',
 214                          'tool_consumer_instance_guid' => '12345-123',
 215                          'resource_link_id' => '4b6fa'
 216                      ],
 217                  ],
 218                  'expected' => [
 219                      'exception' => \coding_exception::class,
 220                      'exception_message' => "Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim"
 221                  ]
 222              ],
 223              'Migrated tool: Legacy data exists, migration claim signature omitted' => [
 224                  'legacy_data' => [
 225                      'consumer_key' => 'CONSUMER_1',
 226                      'tools' => [
 227                          ['secret' => 'toolsecret1'],
 228                          ['secret' => 'toolsecret2'],
 229                      ]
 230                  ],
 231                  'launch_data' => [
 232                      'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
 233                      'launch_migration_claim' => [
 234                          'consumer_key' => 'CONSUMER_1',
 235                          'user_id' => 'user-id-123',
 236                          'context_id' => 'd345b',
 237                          'tool_consumer_instance_guid' => '12345-123',
 238                          'resource_link_id' => '4b6fa'
 239                      ],
 240                  ],
 241                  'expected' => [
 242                      'exception' => \coding_exception::class,
 243                      'exception_message' => "Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."
 244                  ]
 245              ],
 246              'Migrated tool: Legacy data exists, migration claim missing oauth_consumer_key' => [
 247                  'legacy_data' => [
 248                      'consumer_key' => 'CONSUMER_1',
 249                      'tools' => [
 250                          ['secret' => 'toolsecret1'],
 251                          ['secret' => 'toolsecret2'],
 252                      ]
 253                  ],
 254                  'launch_data' => [
 255                      'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
 256                      'launch_migration_claim' => [
 257                          'user_id' => 'user-id-123',
 258                          'context_id' => 'd345b',
 259                          'tool_consumer_instance_guid' => '12345-123',
 260                          'resource_link_id' => '4b6fa'
 261                      ],
 262                  ],
 263                  'expected' => [
 264                      'deployment_consumer_key' => null
 265                  ]
 266              ]
 267          ];
 268      }
 269  
 270      /**
 271       * Test confirming that an exception is thrown if trying to launch a published resource without a custom id.
 272       *
 273       * @covers ::user_launches_tool
 274       */
 275      public function test_user_launches_tool_missing_custom_id() {
 276          $this->resetAfterTest();
 277          [$course, $modresource] = $this->create_test_environment();
 278          $instructoruser = $this->getDataGenerator()->create_user();
 279          $launchservice = $this->get_tool_launch_service();
 280          $mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
 281          $mocklaunch = $this->get_mock_launch($modresource, $mockuser, null, null, false, null, []);
 282  
 283          $this->expectException(\moodle_exception::class);
 284          $this->expectExceptionMessage(get_string('ltiadvlauncherror:missingid', 'enrol_lti'));
 285          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
 286      }
 287  
 288      /**
 289       * Test confirming that an exception is thrown if trying to launch a published resource that doesn't exist.
 290       *
 291       * @covers ::user_launches_tool
 292       */
 293      public function test_user_launches_tool_invalid_custom_id() {
 294          $this->resetAfterTest();
 295          [$course, $modresource] = $this->create_test_environment();
 296          $instructoruser = $this->getDataGenerator()->create_user();
 297          $launchservice = $this->get_tool_launch_service();
 298          $mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
 299          $mocklaunch = $this->get_mock_launch($modresource, $mockuser, null, null, false, null, ['id' => 999999]);
 300  
 301          $this->expectException(\moodle_exception::class);
 302          $this->expectExceptionMessage(get_string('ltiadvlauncherror:invalidid', 'enrol_lti', 999999));
 303          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
 304      }
 305  
 306      /**
 307       * Test confirming that an exception is thrown if trying to launch the tool where no application can be found.
 308       *
 309       * @covers ::user_launches_tool
 310       */
 311      public function test_user_launches_tool_missing_registration() {
 312          $this->resetAfterTest();
 313          // Setup.
 314          [
 315              $course,
 316              $modresource,
 317              $modresource2,
 318              $courseresource,
 319              $registration,
 320              $deployment
 321          ] = $this->create_test_environment();
 322          $instructoruser = $this->getDataGenerator()->create_user();
 323  
 324          // Delete the registration before trying to launch.
 325          $appregrepo = new application_registration_repository();
 326          $appregrepo->delete($registration->get_id());
 327  
 328          // Call the service.
 329          $launchservice = $this->get_tool_launch_service();
 330          $mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
 331          $mocklaunch = $this->get_mock_launch($modresource, $mockuser);
 332  
 333          $this->expectException(\moodle_exception::class);
 334          $this->expectExceptionMessage(get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti',
 335              [$registration->get_platformid(), $registration->get_clientid()]));
 336          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
 337      }
 338  
 339      /**
 340       * Test confirming that an exception is thrown if trying to launch the tool where no deployment can be found.
 341       *
 342       * @covers ::user_launches_tool
 343       */
 344      public function test_user_launches_tool_missing_deployment() {
 345          $this->resetAfterTest();
 346          // Setup.
 347          [
 348              $course,
 349              $modresource,
 350              $modresource2,
 351              $courseresource,
 352              $registration,
 353              $deployment
 354          ] = $this->create_test_environment();
 355          $instructoruser = $this->getDataGenerator()->create_user();
 356  
 357          // Delete the deployment before trying to launch.
 358          $deploymentrepo = new deployment_repository();
 359          $deploymentrepo->delete($deployment->get_id());
 360  
 361          // Call the service.
 362          $launchservice = $this->get_tool_launch_service();
 363          $mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
 364          $mocklaunch = $this->get_mock_launch($modresource, $mockuser);
 365  
 366          $this->expectException(\moodle_exception::class);
 367          $this->expectExceptionMessage(get_string('ltiadvlauncherror:invaliddeployment', 'enrol_lti',
 368              [$deployment->get_deploymentid()]));
 369          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
 370      }
 371  
 372      /**
 373       * Test the mapping from IMS roles to Moodle roles during a launch.
 374       *
 375       * @covers ::user_launches_tool
 376       */
 377      public function test_user_launches_tool_role_mapping() {
 378          $this->resetAfterTest();
 379          // Create mock launches for 3 different user types: instructor, admin, learner.
 380          [$course, $modresource] = $this->create_test_environment();
 381          $instructoruser = $this->getDataGenerator()->create_user();
 382          $instructor2user = $this->getDataGenerator()->create_user();
 383          $adminuser = $this->getDataGenerator()->create_user();
 384          $learneruser = $this->getDataGenerator()->create_user();
 385          $mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
 386          $mockadminuser = $this->get_mock_launch_users_with_ids(
 387              ['2'],
 388              false,
 389              'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
 390          )[0];
 391          $mocklearneruser = $this->get_mock_launch_users_with_ids(
 392              ['3'],
 393              false,
 394              'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
 395          )[0];
 396          $mockinstructor2user = $this->get_mock_launch_users_with_ids(
 397              ['3'],
 398              false,
 399              'Instructor' // Using the legacy (deprecated in 1.3) simple name.
 400          )[0];
 401          $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
 402          $mockadminlaunch = $this->get_mock_launch($modresource, $mockadminuser);
 403          $mocklearnerlaunch = $this->get_mock_launch($modresource, $mocklearneruser);
 404          $mockinstructor2launch = $this->get_mock_launch($modresource, $mockinstructor2user);
 405  
 406          // Launch and confirm the role assignment.
 407          $launchservice = $this->get_tool_launch_service();
 408          $modulecontext = \context::instance_by_id($modresource->contextid);
 409  
 410          [$instructorid] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
 411          [$instructorrole] = array_slice(get_user_roles($modulecontext, $instructorid), 0, 1);
 412          $this->assertEquals('teacher', $instructorrole->shortname);
 413  
 414          [$adminid] = $launchservice->user_launches_tool($adminuser, $mockadminlaunch);
 415          [$adminrole] = array_slice(get_user_roles($modulecontext, $adminid), 0, 1);
 416          $this->assertEquals('teacher', $adminrole->shortname);
 417  
 418          [$learnerid] = $launchservice->user_launches_tool($learneruser, $mocklearnerlaunch);
 419          [$learnerrole] = array_slice(get_user_roles($modulecontext, $learnerid), 0, 1);
 420          $this->assertEquals('student', $learnerrole->shortname);
 421  
 422          [$instructor2id] = $launchservice->user_launches_tool($instructor2user, $mockinstructor2launch);
 423          [$instructor2role] = array_slice(get_user_roles($modulecontext, $instructor2id), 0, 1);
 424          $this->assertEquals('teacher', $instructor2role->shortname);
 425      }
 426  
 427      /**
 428       * Test verifying that a user launch can result in updates to some user fields.
 429       *
 430       * @covers ::user_launches_tool
 431       */
 432      public function test_user_launches_tool_user_fields_updated() {
 433          $this->resetAfterTest();
 434          [$course, $modresource] = $this->create_test_environment();
 435          $user = $this->getDataGenerator()->create_user();
 436          $mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
 437          $launchservice = $this->get_tool_launch_service();
 438          $userrepo = new user_repository();
 439  
 440          // Launch once, verifying the user details.
 441          $mocklaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
 442          $launchservice->user_launches_tool($user, $mocklaunch);
 443          $createduser = $userrepo->find_single_user_by_resource(
 444              $user->id,
 445              $modresource->id
 446          );
 447          $this->assertEquals($modresource->lang, $createduser->get_lang());
 448          $this->assertEquals($modresource->city, $createduser->get_city());
 449          $this->assertEquals($modresource->country, $createduser->get_country());
 450          $this->assertEquals($modresource->institution, $createduser->get_institution());
 451          $this->assertEquals($modresource->timezone, $createduser->get_timezone());
 452          $this->assertEquals($modresource->maildisplay, $createduser->get_maildisplay());
 453  
 454          // Change the resource's defaults and relaunch, verifying the relevant fields are updated for the launch user.
 455          // Note: lang change can't be tested without installation of another language pack.
 456          $modresource->city = 'Paris';
 457          $modresource->country = 'FR';
 458          $modresource->institution = 'Updated institution name';
 459          $modresource->timezone = 'UTC';
 460          $modresource->maildisplay = '1';
 461          global $DB;
 462          $DB->update_record('enrol_lti_tools', $modresource);
 463  
 464          $mocklaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
 465          $launchservice->user_launches_tool($user, $mocklaunch);
 466          $createduser = $userrepo->find($createduser->get_id());
 467          $this->assertEquals($modresource->city, $createduser->get_city());
 468          $this->assertEquals($modresource->country, $createduser->get_country());
 469          $this->assertEquals($modresource->institution, $createduser->get_institution());
 470          $this->assertEquals($modresource->timezone, $createduser->get_timezone());
 471          $this->assertEquals($modresource->maildisplay, $createduser->get_maildisplay());
 472      }
 473  
 474      /**
 475       * Test the launch when a module has an enrolment start date.
 476       *
 477       * @covers ::user_launches_tool
 478       */
 479      public function test_user_launches_tool_max_enrolment_start_restriction() {
 480          $this->resetAfterTest();
 481          [$course, $modresource] = $this->create_test_environment(true, true, false,
 482              \enrol_lti\helper::MEMBER_SYNC_ENROL_NEW, false, false, time() + DAYSECS);
 483          $instructoruser = $this->getDataGenerator()->create_user();
 484          $mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
 485          $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
 486          $launchservice = $this->get_tool_launch_service();
 487  
 488          $this->expectException(\moodle_exception::class);
 489          $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
 490      }
 491  
 492      /**
 493       * Test the Moodle-specific custom param 'forceembed' during user launches.
 494       *
 495       * @covers ::user_launches_tool
 496       */
 497      public function test_user_launches_tool_force_embedding_custom_param() {
 498          $this->resetAfterTest();
 499          [$course, $modresource] = $this->create_test_environment();
 500          $instructoruser = $this->getDataGenerator()->create_user();
 501          $learneruser = $this->getDataGenerator()->create_user();
 502          $mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
 503          $mocklearneruser = $this->get_mock_launch_users_with_ids(['1'], false, '')[0];
 504          $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, null, false, null, [
 505              'id' => $modresource->uuid,
 506              'forcedembed' => true
 507          ]);
 508          $mocklearnerlaunch = $this->get_mock_launch($modresource, $mocklearneruser, null, null, false, null, [
 509              'id' => $modresource->uuid,
 510              'forcedembed' => true
 511          ]);
 512          $launchservice = $this->get_tool_launch_service();
 513          global $SESSION;
 514  
 515          // Instructors aren't subject to forceembed.
 516          $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
 517          $this->assertObjectNotHasAttribute('forcepagelayout', $SESSION);
 518  
 519          // Learners are.
 520          $launchservice->user_launches_tool($learneruser, $mocklearnerlaunch);
 521          $this->assertEquals('embedded', $SESSION->forcepagelayout);
 522      }
 523  
 524      /**
 525       * Test launching the tool with different 'aud' values, confirming the service handles all variations appropriately.
 526       *
 527       * @param mixed $aud the aud value to test
 528       * @param array $expected the array of expectations to check
 529       * @dataProvider aud_data_provider
 530       * @covers ::user_launches_tool
 531       */
 532      public function test_user_launches_tool_aud_variations($aud, array $expected) {
 533          $this->resetAfterTest();
 534          [$course, $modresource] = $this->create_test_environment();
 535          $instructoruser = $this->getDataGenerator()->create_user();
 536          $mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
 537          $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, null, false, null, [
 538              'id' => $modresource->uuid,
 539          ], $aud);
 540  
 541          $launchservice = $this->get_tool_launch_service();
 542          if (isset($expected['exception'])) {
 543              $this->expectException($expected['exception']);
 544              $this->expectExceptionMessage($expected['exceptionmessage']);
 545          }
 546          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
 547  
 548          $this->assertNotEmpty($userid);
 549          $this->assertNotEmpty($resource);
 550      }
 551  
 552      /**
 553       * Data provider for testing variations of the 'aud' JWT property.
 554       *
 555       * @return array the test case data
 556       */
 557      public function aud_data_provider(): array {
 558          return [
 559              'valid, array having multiple entries with the first one being clientid' => [
 560                  'aud' => ['123', 'something else', 'blah'],
 561                  'expected' => [
 562                      'valid' => true
 563                  ]
 564              ],
 565              'valid, array having a single entry being clientid' => [
 566                  'aud' => ['123'],
 567                  'expected' => [
 568                      'valid' => true
 569                  ]
 570              ],
 571              'valid, string containing the single clientid' => [
 572                  'aud' => '123',
 573                  'expected' => [
 574                      'valid' => true
 575                  ]
 576              ],
 577              'invalid, array having multiple values where the first item is not clientid' => [
 578                  'aud' => ['cat', 'dog', '123'],
 579                  'expected' => [
 580                      'valid' => false,
 581                      'exception' => \moodle_exception::class,
 582                      'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti')
 583                  ]
 584              ],
 585              'invalid, array containing a single item which is not clientid' => [
 586                  'aud' => ['cat'],
 587                  'expected' => [
 588                      'valid' => false,
 589                      'exception' => \moodle_exception::class,
 590                      'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti')
 591                  ]
 592              ],
 593              'invalid, string contains the item and it is not the clientid' => [
 594                  'aud' => 'cat',
 595                  'expected' => [
 596                      'valid' => false,
 597                      'exception' => \moodle_exception::class,
 598                      'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti')
 599                  ]
 600              ],
 601              'invalid, empty string' => [
 602                  'aud' => '',
 603                  'expected' => [
 604                      'valid' => false,
 605                      'exception' => \moodle_exception::class,
 606                      'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti')
 607                  ]
 608              ],
 609          ];
 610      }
 611  
 612      /**
 613       * Test verifying how changes to lti-ags claim information is handled across different launches.
 614       *
 615       * @param array $agsclaim1 the lti-ags claim data to use in the first launch.
 616       * @param array $agsclaim2 the lti-ags claim data to use in the second launch.
 617       * @param array $expected the array of test case expectations.
 618       * @dataProvider ags_claim_provider
 619       * @covers ::user_launches_tool
 620       */
 621      public function test_user_launches_tool_ags_claim_handling(array $agsclaim1, array $agsclaim2, array $expected) {
 622          $this->resetAfterTest();
 623          [$course, $modresource] = $this->create_test_environment();
 624          $instructoruser = $this->getDataGenerator()->create_user();
 625          $mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
 626          $userrepo = new user_repository();
 627          $resourcelinkrepo = new resource_link_repository();
 628          $launchservice = $this->get_tool_launch_service();
 629  
 630          // Launch the first time.
 631          $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, $agsclaim1, false, null, [
 632              'id' => $modresource->uuid,
 633          ]);
 634          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
 635  
 636          $ltiuser = $userrepo->find_single_user_by_resource($userid, $resource->id);
 637          $resourcelink = $resourcelinkrepo->find_by_resource_and_user($resource->id, $ltiuser->get_id())[0];
 638          $gradeservice = $resourcelink->get_grade_service();
 639          $lineitemurl = $agsclaim1['lineitem'] ?? null;
 640          $lineitemsurl = $agsclaim1['lineitems'] ?? null;
 641          $this->assertEquals($agsclaim1['scope'], $gradeservice->get_scopes());
 642          $this->assertEquals($lineitemurl, $gradeservice->get_lineitemurl());
 643          $this->assertEquals($lineitemsurl, $gradeservice->get_lineitemsurl());
 644  
 645          // Launch again, with a new lti-ags claim.
 646          $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, $agsclaim2, false, null, [
 647              'id' => $modresource->uuid,
 648          ]);
 649          [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
 650          $ltiuser = $userrepo->find_single_user_by_resource($userid, $resource->id);
 651          $resourcelink = $resourcelinkrepo->find_by_resource_and_user($resource->id, $ltiuser->get_id())[0];
 652          $gradeservice = $resourcelink->get_grade_service();
 653          $lineitemurl = $expected['lineitem'] ?? null;
 654          $lineitemsurl = $expected['lineitems'] ?? null;
 655          $this->assertEquals($expected['scope'], $gradeservice->get_scopes());
 656          $this->assertEquals($lineitemurl, $gradeservice->get_lineitemurl());
 657          $this->assertEquals($lineitemsurl, $gradeservice->get_lineitemsurl());
 658      }
 659  
 660      /**
 661       * Data provider for testing user_launches tool with varying mocked lti-ags claim data over several launches.
 662       *
 663       * @return array the array of test case data.
 664       */
 665      public function ags_claim_provider(): array {
 666          return [
 667              'Coupled line item with score post only, no change to scopes on subsequent launch' => [
 668                  'agsclaim1' => [
 669                      "scope" => [
 670                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 671                      ],
 672                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 673                  ],
 674                  'agsclaim2' => [
 675                      "scope" => [
 676                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 677                      ],
 678                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 679                  ],
 680                  'expected' => [
 681                      "scope" => [
 682                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 683                      ],
 684                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 685                  ]
 686              ],
 687              'Coupled line item with score post only, addition to scopes on subsequent launch' => [
 688                  'agsclaim1' => [
 689                      "scope" => [
 690                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 691                      ],
 692                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 693                  ],
 694                  'agsclaim2' => [
 695                      "scope" => [
 696                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 697                          "https://purl.imsglobal.org/spec/lti-ags/scope/score",
 698                      ],
 699                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 700                  ],
 701                  'expected' => [
 702                      "scope" => [
 703                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 704                          "https://purl.imsglobal.org/spec/lti-ags/scope/score",
 705                      ],
 706                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 707                  ]
 708              ],
 709              'Coupled line item with score post + result read, removal of scopes on subsequent launch' => [
 710                  'agsclaim1' => [
 711                      "scope" => [
 712                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 713                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 714                      ],
 715                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 716                  ],
 717                  'agsclaim2' => [
 718                      "scope" => [
 719                          "https://purl.imsglobal.org/spec/lti-ags/scope/score",
 720                      ],
 721                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 722                  ],
 723                  'expected' => [
 724                      "scope" => [
 725                          "https://purl.imsglobal.org/spec/lti-ags/scope/score",
 726                      ],
 727                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 728                  ]
 729              ],
 730              'Decoupled line items with all capabilities, change and removal of scopes on subsequent launch' => [
 731                  'agsclaim1' => [
 732                      "scope" => [
 733                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 734                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 735                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 736                      ],
 737                      "lineitems" => "https://platform.example.com/10/lineitems/",
 738                  ],
 739                  'agsclaim2' => [
 740                      "scope" => [
 741                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
 742                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 743                      ],
 744                      "lineitems" => "https://platform.example.com/10/lineitems/",
 745                  ],
 746                  'expected' => [
 747                      "scope" => [
 748                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
 749                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 750                      ],
 751                      "lineitems" => "https://platform.example.com/10/lineitems/",
 752                  ]
 753              ],
 754              'Decoupled line items with all capabilities, removal of scopes on subsequent launch' => [
 755                  'agsclaim1' => [
 756                      "scope" => [
 757                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 758                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 759                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 760                      ],
 761                      "lineitems" => "https://platform.example.com/10/lineitems/",
 762                  ],
 763                  'agsclaim2' => [
 764                      "scope" => [
 765                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 766                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 767                      ],
 768                      "lineitems" => "https://platform.example.com/10/lineitems/",
 769                  ],
 770                  'expected' => [
 771                      "scope" => [
 772                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 773                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 774                      ],
 775                      "lineitems" => "https://platform.example.com/10/lineitems/",
 776                  ]
 777              ],
 778              'Coupled line items with score post only, addition of lineitemsurl and all capabilities on subsequent launch' => [
 779                  'agsclaim1' => [
 780                      "scope" => [
 781                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 782                      ],
 783                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 784                  ],
 785                  'agsclaim2' => [
 786                      "scope" => [
 787                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 788                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 789                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 790                      ],
 791                      "lineitems" => "https://platform.example.com/10/lineitems/",
 792                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 793                  ],
 794                  'expected' => [
 795                      "scope" => [
 796                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 797                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 798                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 799                      ],
 800                      "lineitems" => "https://platform.example.com/10/lineitems/",
 801                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 802                  ]
 803              ],
 804              'Decoupled line items with all capabilities, change to coupled line item with score post only on subsequent launch' => [
 805                  'agsclaim1' => [
 806                      "scope" => [
 807                          "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
 808                          "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
 809                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 810                      ],
 811                      "lineitems" => "https://platform.example.com/10/lineitems/",
 812                  ],
 813                  'agsclaim2' => [
 814                      "scope" => [
 815                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 816                      ],
 817                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 818                  ],
 819                  'expected' => [
 820                      "scope" => [
 821                          "https://purl.imsglobal.org/spec/lti-ags/scope/score"
 822                      ],
 823                      "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
 824                  ]
 825              ],
 826          ];
 827      }
 828  }