See Release Notes
Long Term Support Release
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace enrol_lti\local\ltiadvantage\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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body